From b9fe7ff168446060f857ee75c3d655477a7f924c Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 01:11:00 +0800 Subject: [PATCH] fix: batch favorite status checks --- src/favorites/FavoritesProvider.tsx | 95 ++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/src/favorites/FavoritesProvider.tsx b/src/favorites/FavoritesProvider.tsx index 7e93cb8..3c4c082 100644 --- a/src/favorites/FavoritesProvider.tsx +++ b/src/favorites/FavoritesProvider.tsx @@ -36,19 +36,52 @@ export function FavoritesProvider({ children }: { children: ReactNode }) { const [pendingIds, setPendingIds] = useState>(() => new Set()); const pendingAfterLoginRef = useRef(null); const lastAddressRef = useRef(null); + const knownIdsRef = useRef>(new Set()); + const inFlightIdsRef = useRef>(new Set()); + const queuedIdsRef = useRef>(new Set()); + const batchTimerRef = useRef(null); + const tokenRef = useRef(null); + + useEffect(() => { + tokenRef.current = status === "loggedIn" ? token : null; + }, [status, token]); + + const clearFavoriteStatus = useCallback(() => { + knownIdsRef.current = new Set(); + inFlightIdsRef.current = new Set(); + queuedIdsRef.current = new Set(); + if (batchTimerRef.current !== null) { + window.clearTimeout(batchTimerRef.current); + batchTimerRef.current = null; + } + setFavoriteIds(new Set()); + setKnownIds(new Set()); + setPendingIds(new Set()); + }, []); useEffect(() => { const nextAddress = status === "loggedIn" ? address : null; if (lastAddressRef.current === nextAddress) return; lastAddressRef.current = nextAddress; - setFavoriteIds(new Set()); - setKnownIds(new Set()); - setPendingIds(new Set()); + clearFavoriteStatus(); if (!nextAddress) pendingAfterLoginRef.current = null; - }, [address, status]); + }, [address, clearFavoriteStatus, status]); + + useEffect( + () => () => { + if (batchTimerRef.current !== null) { + window.clearTimeout(batchTimerRef.current); + } + }, + [], + ); const markFavorite = useCallback((resourceId: string, favorited: boolean) => { - setKnownIds((prev) => new Set(prev).add(resourceId)); + setKnownIds((prev) => { + const next = new Set(prev).add(resourceId); + knownIdsRef.current = next; + return next; + }); setFavoriteIds((prev) => { const next = new Set(prev); if (favorited) next.add(resourceId); @@ -57,17 +90,22 @@ export function FavoritesProvider({ children }: { children: ReactNode }) { }); }, []); - const ensureFavoriteIds = useCallback( - async (resourceIds: string[]) => { - if (!token || status !== "loggedIn") return; - const missing = [...new Set(resourceIds)].filter( - (id) => !knownIds.has(id), - ); - if (missing.length === 0) return; - const ids = await getFavoriteIds(token, missing); + const flushFavoriteIdBatch = useCallback(async () => { + const requestToken = tokenRef.current; + const requestIds = Array.from(queuedIdsRef.current); + queuedIdsRef.current.clear(); + if (!requestToken || requestIds.length === 0) { + requestIds.forEach((id) => inFlightIdsRef.current.delete(id)); + return; + } + + try { + const ids = await getFavoriteIds(requestToken, requestIds); + if (tokenRef.current !== requestToken) return; setKnownIds((prev) => { const next = new Set(prev); - missing.forEach((id) => next.add(id)); + requestIds.forEach((id) => next.add(id)); + knownIdsRef.current = next; return next; }); setFavoriteIds((prev) => { @@ -75,8 +113,35 @@ export function FavoritesProvider({ children }: { children: ReactNode }) { ids.forEach((id) => next.add(id)); return next; }); + } finally { + requestIds.forEach((id) => inFlightIdsRef.current.delete(id)); + if (queuedIdsRef.current.size > 0 && batchTimerRef.current === null) { + batchTimerRef.current = window.setTimeout(() => { + batchTimerRef.current = null; + void flushFavoriteIdBatch(); + }, 0); + } + } + }, []); + + const ensureFavoriteIds = useCallback( + async (resourceIds: string[]) => { + if (!token || status !== "loggedIn") return; + const missing = [...new Set(resourceIds)].filter( + (id) => !knownIdsRef.current.has(id) && !inFlightIdsRef.current.has(id), + ); + if (missing.length === 0) return; + missing.forEach((id) => { + queuedIdsRef.current.add(id); + inFlightIdsRef.current.add(id); + }); + if (batchTimerRef.current !== null) return; + batchTimerRef.current = window.setTimeout(() => { + batchTimerRef.current = null; + void flushFavoriteIdBatch(); + }, 0); }, - [knownIds, status, token], + [flushFavoriteIdBatch, status, token], ); const runFavoriteMutation = useCallback(