Skip to content

Commit

Permalink
refactor(mobile): prefetch entry logic
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoban committed Feb 5, 2025
1 parent 03a1438 commit fb266f4
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 89 deletions.
10 changes: 4 additions & 6 deletions apps/mobile/src/modules/entry-list/entry-list-gird.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type { MasonryFlashListProps } from "@shopify/flash-list"
import { MasonryFlashList } from "@shopify/flash-list"
import { Image } from "expo-image"
import { Link } from "expo-router"
import { useContext, useState } from "react"
import { useContext } from "react"
import { Pressable, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

import { NavigationContext } from "@/src/components/common/SafeNavigationScrollView"
import { ThemedText } from "@/src/components/common/ThemedText"
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
import { useEntry, useFetchEntryContentByStream } from "@/src/store/entry/hooks"
import { useEntry } from "@/src/store/entry/hooks"
import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"

import { useSelectedFeed } from "../feed-drawer/atoms"

Expand All @@ -23,9 +24,6 @@ export function EntryListContentGrid({
}: {
entryIds: string[]
} & Omit<MasonryFlashListProps<string>, "data" | "renderItem">) {
const [viewableEntryIds, setViewableEntryIds] = useState<string[]>([])
useFetchEntryContentByStream(viewableEntryIds)

const insets = useSafeAreaInsets()
const tabBarHeight = useBottomTabBarHeight()
const headerHeight = useHeaderHeight()
Expand All @@ -38,7 +36,7 @@ export function EntryListContentGrid({
}, [])}
keyExtractor={(id) => id}
onViewableItemsChanged={({ viewableItems }) => {
setViewableEntryIds(viewableItems.map((item) => item.key))
debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key))
}}
numColumns={2}
onScroll={useTypeScriptHappyCallback(
Expand Down
10 changes: 4 additions & 6 deletions apps/mobile/src/modules/entry-list/entry-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"
import { FlashList } from "@shopify/flash-list"
import { Image } from "expo-image"
import { router } from "expo-router"
import { useCallback, useContext, useMemo, useState } from "react"
import { useCallback, useContext, useMemo } from "react"
import { StyleSheet, Text, useAnimatedValue, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

Expand All @@ -15,7 +15,8 @@ import {
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
import { useDefaultHeaderHeight } from "@/src/hooks/useDefaultHeaderHeight"
import { useSelectedFeed, useSelectedFeedTitle } from "@/src/modules/feed-drawer/atoms"
import { useEntry, useFetchEntryContentByStream } from "@/src/store/entry/hooks"
import { useEntry } from "@/src/store/entry/hooks"
import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"

import { ViewSelector } from "../feed-drawer/view-selector"
import { LeftAction, RightAction } from "./action"
Expand Down Expand Up @@ -59,9 +60,6 @@ export function EntryListScreen({ entryIds }: { entryIds: string[] }) {
}

function EntryListContent({ entryIds }: { entryIds: string[] }) {
const [viewableEntryIds, setViewableEntryIds] = useState<string[]>([])
useFetchEntryContentByStream(viewableEntryIds)

const insets = useSafeAreaInsets()
const tabBarHeight = useBottomTabBarHeight()
const originalDefaultHeaderHeight = useDefaultHeaderHeight()
Expand All @@ -84,7 +82,7 @@ function EntryListContent({ entryIds }: { entryIds: string[] }) {
)}
keyExtractor={(id) => id}
onViewableItemsChanged={({ viewableItems }) => {
setViewableEntryIds(viewableItems.map((item) => item.key))
debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key))
}}
scrollIndicatorInsets={{
top: headerHeight - insets.top,
Expand Down
79 changes: 2 additions & 77 deletions apps/mobile/src/store/entry/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type { FeedViewType } from "@follow/constants"
import { useMutation, useQuery } from "@tanstack/react-query"
import { fetch } from "expo/fetch"
import { useCallback, useEffect } from "react"

import { apiClient } from "@/src/lib/api-fetch"
import { getCookie } from "@/src/lib/auth"

import { getEntry } from "./getter"
import { entryActions, entrySyncServices, useEntryStore } from "./store"
import { entrySyncServices, useEntryStore } from "./store"
import type { EntryModel, FetchEntriesProps } from "./types"

export const usePrefetchEntries = (props: FetchEntriesProps) => {
Expand Down Expand Up @@ -90,78 +86,7 @@ export const useEntryIdsByCategory = (category: string) => {
export const useFetchEntryContentByStream = (remoteEntryIds?: string[]) => {
const { mutate: updateEntryContent } = useMutation({
mutationKey: ["stream-entry-content", remoteEntryIds],
mutationFn: async (remoteEntryIds: string[]) => {
const onlyNoStored = true

const nextIds = [] as string[]
if (onlyNoStored) {
for (const id of remoteEntryIds) {
const entry = getEntry(id)!
if (entry.content) {
continue
}

nextIds.push(id)
}
}

if (nextIds.length === 0) return

const readStream = async () => {
// https://github.com/facebook/react-native/issues/37505
// TODO: And it seems we can not just use fetch from expo for ofetch, need further investigation
const response = await fetch(apiClient.entries.stream.$url().toString(), {
method: "post",
headers: {
cookie: getCookie(),
},
body: JSON.stringify({
ids: nextIds,
}),
})

const reader = response.body?.getReader()
if (!reader) return

const decoder = new TextDecoder()
let buffer = ""

try {
while (true) {
const { done, value } = await reader.read()
if (done) break

buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")

// Process all complete lines
for (let i = 0; i < lines.length - 1; i++) {
if (lines[i]!.trim()) {
const json = JSON.parse(lines[i]!)
// Handle each JSON line here
entryActions.updateEntryContent(json.id, json.content)
}
}

// Keep the last incomplete line in the buffer
buffer = lines.at(-1) || ""
}

// Process any remaining data
if (buffer.trim()) {
const json = JSON.parse(buffer)

entryActions.updateEntryContent(json.id, json.content)
}
} catch (error) {
console.error("Error reading stream:", error)
} finally {
reader.releaseLock()
}
}

readStream()
},
mutationFn: entrySyncServices.fetchEntryContentByStream,
})

useEffect(() => {
Expand Down
82 changes: 82 additions & 0 deletions apps/mobile/src/store/entry/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { FeedViewType } from "@follow/constants"
import { debounce } from "es-toolkit/compat"
import { fetch as expoFetch } from "expo/fetch"

import { apiClient } from "@/src/lib/api-fetch"
import { getCookie } from "@/src/lib/auth"
import { honoMorph } from "@/src/morph/hono"
import { storeDbMorph } from "@/src/morph/store-db"
import { EntryService } from "@/src/services/entry"
Expand Down Expand Up @@ -215,7 +218,86 @@ class EntrySyncServices {
}
return entry
}

async fetchEntryContentByStream(remoteEntryIds?: string[]) {
if (!remoteEntryIds || remoteEntryIds.length === 0) return

const onlyNoStored = true

const nextIds = [] as string[]
if (onlyNoStored) {
for (const id of remoteEntryIds) {
const entry = getEntry(id)!
if (entry.content) {
continue
}

nextIds.push(id)
}
}

if (nextIds.length === 0) return

const readStream = async () => {
// https://github.com/facebook/react-native/issues/37505
// TODO: And it seems we can not just use fetch from expo for ofetch, need further investigation
const response = await expoFetch(apiClient.entries.stream.$url().toString(), {
method: "post",
headers: {
cookie: getCookie(),
},
body: JSON.stringify({
ids: nextIds,
}),
})

const reader = response.body?.getReader()
if (!reader) return

const decoder = new TextDecoder()
let buffer = ""

try {
while (true) {
const { done, value } = await reader.read()
if (done) break

buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")

// Process all complete lines
for (let i = 0; i < lines.length - 1; i++) {
if (lines[i]!.trim()) {
const json = JSON.parse(lines[i]!)
// Handle each JSON line here
entryActions.updateEntryContent(json.id, json.content)
}
}

// Keep the last incomplete line in the buffer
buffer = lines.at(-1) || ""
}

// Process any remaining data
if (buffer.trim()) {
const json = JSON.parse(buffer)

entryActions.updateEntryContent(json.id, json.content)
}
} catch (error) {
console.error("Error reading stream:", error)
} finally {
reader.releaseLock()
}
}

readStream()
}
}

export const entrySyncServices = new EntrySyncServices()
export const entryActions = new EntryActions()
export const debouncedFetchEntryContentByStream = debounce(
entrySyncServices.fetchEntryContentByStream,
1000,
)

0 comments on commit fb266f4

Please sign in to comment.