From 77d45897b27ef9bd4d5a3f7afa9530a1fc81e574 Mon Sep 17 00:00:00 2001 From: lemma <> Date: Sat, 6 Jun 2026 10:26:31 -0500 Subject: [PATCH] List subscriptions in batches, handling state properly --- app/lib/api-client.ts | 2 +- app/lib/data-context.tsx | 20 +++++++--- app/routes/subscriptions.tsx | 75 +++++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/app/lib/api-client.ts b/app/lib/api-client.ts index cbee1b3..e6c9f92 100644 --- a/app/lib/api-client.ts +++ b/app/lib/api-client.ts @@ -86,7 +86,7 @@ type FeedPage = { cursor: string | null } -type Subscription = { +export type Subscription = { rkey: string publicationAtUri: string authorDid: string diff --git a/app/lib/data-context.tsx b/app/lib/data-context.tsx index bdf1400..5c3d7f8 100644 --- a/app/lib/data-context.tsx +++ b/app/lib/data-context.tsx @@ -3,21 +3,23 @@ import { useAuth } from '~/lib/auth-context' import { backfillPosts, backfillPublications, - backfillSubscriptions, getUserBlogPosts, listPublications, listSubscriptions, SessionExpiredError, type Post, type Publication, + type Subscription, } from '~/lib/api-client' type DataContextValue = { publications: Publication[] | undefined posts: Post[] | undefined + subscriptions: Subscription[] | undefined setPublications: (pubs: Publication[]) => void invalidatePosts: () => void removePost: (rkey: string) => void + removeSubscription: (rkey: string) => void } const DataContext = createContext(null) @@ -41,6 +43,7 @@ export function DataProvider({ children }: { children: React.ReactNode }) { const [publications, setPublications] = useState(undefined) const [posts, setPosts] = useState(undefined) const [postsRevision, setPostsRevision] = useState(0) + const [subscriptions, setSubscriptions] = useState(undefined) useEffect(() => { if (!session) { @@ -99,15 +102,19 @@ export function DataProvider({ children }: { children: React.ReactNode }) { }, [session, postsRevision, signOut]) useEffect(() => { - if (!session) return + if (!session) { + setSubscriptions(undefined) + return + } let cancelled = false ;(async () => { try { const subs = await listSubscriptions(session) - if (!cancelled && subs.length === 0) await backfillSubscriptions(session) + if (!cancelled) setSubscriptions(subs) } catch (err) { if (err instanceof SessionExpiredError) { signOut(); return } - console.error('Failed to check subscriptions:', err) + console.error('Failed to load subscriptions:', err) + if (!cancelled) setSubscriptions([]) } })() return () => { cancelled = true } @@ -118,9 +125,12 @@ export function DataProvider({ children }: { children: React.ReactNode }) { const removePost = useCallback((rkey: string) => { setPosts((prev) => prev?.filter((p) => p.atUri.split('/').pop() !== rkey) ?? prev) }, []) + const removeSubscription = useCallback((rkey: string) => { + setSubscriptions((prev) => prev?.filter((s) => s.rkey !== rkey) ?? prev) + }, []) return ( - + {children} ) diff --git a/app/routes/subscriptions.tsx b/app/routes/subscriptions.tsx index 5161df2..852ad9a 100644 --- a/app/routes/subscriptions.tsx +++ b/app/routes/subscriptions.tsx @@ -1,9 +1,7 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useAuth } from '~/lib/auth-context' -import { - listSubscriptions, listPublicPublications, unsubscribe, - type Publication, -} from '~/lib/api-client' +import { useData } from '~/lib/data-context' +import { listPublicPublications, unsubscribe, type Publication } from '~/lib/api-client' import CenteredMessage from '~/components/CenteredMessage' import EmptyState from '~/components/EmptyState' import PageLayout from '~/components/PageLayout' @@ -18,53 +16,70 @@ type SubscribedPub = Publication & { rkey: string } export default function Subscriptions() { const { session, loading: authLoading } = useAuth() + const { subscriptions, removeSubscription } = useData() const [pubs, setPubs] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [loadingPubs, setLoadingPubs] = useState>({}) + const loadedRef = useRef(false) useEffect(() => { - if (authLoading || !session) return + // subscriptions undefined means DataProvider hasn't finished loading yet + if (subscriptions === undefined || loadedRef.current) return + loadedRef.current = true + + if (subscriptions.length === 0) { + setPubs([]) + return + } + let cancelled = false setLoading(true) - async function load() { - const subs = await listSubscriptions(session!) - if (cancelled) return + const byAuthor = new Map() + for (const s of subscriptions) { + const list = byAuthor.get(s.authorDid) ?? [] + list.push({ rkey: s.rkey, publicationAtUri: s.publicationAtUri }) + byAuthor.set(s.authorDid, list) + } - const byAuthor = new Map() - for (const s of subs) { - const list = byAuthor.get(s.authorDid) ?? [] - list.push({ rkey: s.rkey, publicationAtUri: s.publicationAtUri }) - byAuthor.set(s.authorDid, list) + const authors = [...byAuthor.entries()] + const BATCH = 5 + const allResults: SubscribedPub[] = [] + + async function fetchBatched() { + for (let i = 0; i < authors.length; i += BATCH) { + if (cancelled) return + const batch = authors.slice(i, i + BATCH) + const results = await Promise.allSettled( + batch.map(async ([did, entries]) => { + const pubList = await listPublicPublications(did) + return entries.flatMap(({ rkey, publicationAtUri }) => { + const pub = pubList.find((p) => p.atUri === publicationAtUri) + return pub ? [{ ...pub, rkey }] : [] + }) + }), + ) + for (const r of results) { + if (r.status === 'fulfilled') allResults.push(...r.value) + } } - - const results = await Promise.allSettled( - [...byAuthor.entries()].map(async ([did, entries]) => { - const pubList = await listPublicPublications(did) - return entries.flatMap(({ rkey, publicationAtUri }) => { - const pub = pubList.find((p) => p.atUri === publicationAtUri) - return pub ? [{ ...pub, rkey }] : [] - }) - }), - ) - - if (cancelled) return - setPubs(results.flatMap((r) => (r.status === 'fulfilled' ? r.value : []))) + if (!cancelled) setPubs(allResults) } - load() + fetchBatched() .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load') }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } - }, [session, authLoading]) + }, [subscriptions]) async function handleUnsubscribe(pub: SubscribedPub) { if (!session) return setLoadingPubs((prev) => ({ ...prev, [pub.atUri]: true })) try { await unsubscribe(session, pub.rkey) + removeSubscription(pub.rkey) setPubs((prev) => prev?.filter((p) => p.atUri !== pub.atUri) ?? prev) } catch { // non-fatal @@ -73,7 +88,7 @@ export default function Subscriptions() { } } - if (authLoading) { + if (authLoading || (session && subscriptions === undefined)) { return (
-- 2.51.2