Blogging platform with advanced tools for arts and sciences.
6

Configure Feed

Select the types of activity you want to include in your feed.

List subscriptions in batches, handling state properly

lemma (Jun 6, 2026, 10:26 AM -0500) 77d45897 9ca91208

+61 -36
+1 -1
app/lib/api-client.ts
··· 86 86 cursor: string | null 87 87 } 88 88 89 - type Subscription = { 89 + export type Subscription = { 90 90 rkey: string 91 91 publicationAtUri: string 92 92 authorDid: string
+15 -5
app/lib/data-context.tsx
··· 3 3 import { 4 4 backfillPosts, 5 5 backfillPublications, 6 - backfillSubscriptions, 7 6 getUserBlogPosts, 8 7 listPublications, 9 8 listSubscriptions, 10 9 SessionExpiredError, 11 10 type Post, 12 11 type Publication, 12 + type Subscription, 13 13 } from '~/lib/api-client' 14 14 15 15 type DataContextValue = { 16 16 publications: Publication[] | undefined 17 17 posts: Post[] | undefined 18 + subscriptions: Subscription[] | undefined 18 19 setPublications: (pubs: Publication[]) => void 19 20 invalidatePosts: () => void 20 21 removePost: (rkey: string) => void 22 + removeSubscription: (rkey: string) => void 21 23 } 22 24 23 25 const DataContext = createContext<DataContextValue | null>(null) ··· 41 43 const [publications, setPublications] = useState<Publication[] | undefined>(undefined) 42 44 const [posts, setPosts] = useState<Post[] | undefined>(undefined) 43 45 const [postsRevision, setPostsRevision] = useState(0) 46 + const [subscriptions, setSubscriptions] = useState<Subscription[] | undefined>(undefined) 44 47 45 48 useEffect(() => { 46 49 if (!session) { ··· 99 102 }, [session, postsRevision, signOut]) 100 103 101 104 useEffect(() => { 102 - if (!session) return 105 + if (!session) { 106 + setSubscriptions(undefined) 107 + return 108 + } 103 109 let cancelled = false 104 110 ;(async () => { 105 111 try { 106 112 const subs = await listSubscriptions(session) 107 - if (!cancelled && subs.length === 0) await backfillSubscriptions(session) 113 + if (!cancelled) setSubscriptions(subs) 108 114 } catch (err) { 109 115 if (err instanceof SessionExpiredError) { signOut(); return } 110 - console.error('Failed to check subscriptions:', err) 116 + console.error('Failed to load subscriptions:', err) 117 + if (!cancelled) setSubscriptions([]) 111 118 } 112 119 })() 113 120 return () => { cancelled = true } ··· 118 125 const removePost = useCallback((rkey: string) => { 119 126 setPosts((prev) => prev?.filter((p) => p.atUri.split('/').pop() !== rkey) ?? prev) 120 127 }, []) 128 + const removeSubscription = useCallback((rkey: string) => { 129 + setSubscriptions((prev) => prev?.filter((s) => s.rkey !== rkey) ?? prev) 130 + }, []) 121 131 122 132 return ( 123 - <DataContext.Provider value={{ publications, posts, setPublications: handleSetPublications, invalidatePosts, removePost }}> 133 + <DataContext.Provider value={{ publications, posts, subscriptions, setPublications: handleSetPublications, invalidatePosts, removePost, removeSubscription }}> 124 134 {children} 125 135 </DataContext.Provider> 126 136 )
+45 -30
app/routes/subscriptions.tsx
··· 1 - import { useEffect, useState } from 'react' 1 + import { useEffect, useRef, useState } from 'react' 2 2 import { useAuth } from '~/lib/auth-context' 3 - import { 4 - listSubscriptions, listPublicPublications, unsubscribe, 5 - type Publication, 6 - } from '~/lib/api-client' 3 + import { useData } from '~/lib/data-context' 4 + import { listPublicPublications, unsubscribe, type Publication } from '~/lib/api-client' 7 5 import CenteredMessage from '~/components/CenteredMessage' 8 6 import EmptyState from '~/components/EmptyState' 9 7 import PageLayout from '~/components/PageLayout' ··· 18 16 19 17 export default function Subscriptions() { 20 18 const { session, loading: authLoading } = useAuth() 19 + const { subscriptions, removeSubscription } = useData() 21 20 const [pubs, setPubs] = useState<SubscribedPub[] | null>(null) 22 21 const [loading, setLoading] = useState(false) 23 22 const [error, setError] = useState<string | null>(null) 24 23 const [loadingPubs, setLoadingPubs] = useState<Record<string, boolean>>({}) 24 + const loadedRef = useRef(false) 25 25 26 26 useEffect(() => { 27 - if (authLoading || !session) return 27 + // subscriptions undefined means DataProvider hasn't finished loading yet 28 + if (subscriptions === undefined || loadedRef.current) return 29 + loadedRef.current = true 30 + 31 + if (subscriptions.length === 0) { 32 + setPubs([]) 33 + return 34 + } 35 + 28 36 let cancelled = false 29 37 setLoading(true) 30 38 31 - async function load() { 32 - const subs = await listSubscriptions(session!) 33 - if (cancelled) return 39 + const byAuthor = new Map<string, { rkey: string; publicationAtUri: string }[]>() 40 + for (const s of subscriptions) { 41 + const list = byAuthor.get(s.authorDid) ?? [] 42 + list.push({ rkey: s.rkey, publicationAtUri: s.publicationAtUri }) 43 + byAuthor.set(s.authorDid, list) 44 + } 45 + 46 + const authors = [...byAuthor.entries()] 47 + const BATCH = 5 48 + const allResults: SubscribedPub[] = [] 34 49 35 - const byAuthor = new Map<string, { rkey: string; publicationAtUri: string }[]>() 36 - for (const s of subs) { 37 - const list = byAuthor.get(s.authorDid) ?? [] 38 - list.push({ rkey: s.rkey, publicationAtUri: s.publicationAtUri }) 39 - byAuthor.set(s.authorDid, list) 50 + async function fetchBatched() { 51 + for (let i = 0; i < authors.length; i += BATCH) { 52 + if (cancelled) return 53 + const batch = authors.slice(i, i + BATCH) 54 + const results = await Promise.allSettled( 55 + batch.map(async ([did, entries]) => { 56 + const pubList = await listPublicPublications(did) 57 + return entries.flatMap(({ rkey, publicationAtUri }) => { 58 + const pub = pubList.find((p) => p.atUri === publicationAtUri) 59 + return pub ? [{ ...pub, rkey }] : [] 60 + }) 61 + }), 62 + ) 63 + for (const r of results) { 64 + if (r.status === 'fulfilled') allResults.push(...r.value) 65 + } 40 66 } 41 - 42 - const results = await Promise.allSettled( 43 - [...byAuthor.entries()].map(async ([did, entries]) => { 44 - const pubList = await listPublicPublications(did) 45 - return entries.flatMap(({ rkey, publicationAtUri }) => { 46 - const pub = pubList.find((p) => p.atUri === publicationAtUri) 47 - return pub ? [{ ...pub, rkey }] : [] 48 - }) 49 - }), 50 - ) 51 - 52 - if (cancelled) return 53 - setPubs(results.flatMap((r) => (r.status === 'fulfilled' ? r.value : []))) 67 + if (!cancelled) setPubs(allResults) 54 68 } 55 69 56 - load() 70 + fetchBatched() 57 71 .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load') }) 58 72 .finally(() => { if (!cancelled) setLoading(false) }) 59 73 60 74 return () => { cancelled = true } 61 - }, [session, authLoading]) 75 + }, [subscriptions]) 62 76 63 77 async function handleUnsubscribe(pub: SubscribedPub) { 64 78 if (!session) return 65 79 setLoadingPubs((prev) => ({ ...prev, [pub.atUri]: true })) 66 80 try { 67 81 await unsubscribe(session, pub.rkey) 82 + removeSubscription(pub.rkey) 68 83 setPubs((prev) => prev?.filter((p) => p.atUri !== pub.atUri) ?? prev) 69 84 } catch { 70 85 // non-fatal ··· 73 88 } 74 89 } 75 90 76 - if (authLoading) { 91 + if (authLoading || (session && subscriptions === undefined)) { 77 92 return ( 78 93 <div className="flex-1 flex items-center justify-center"> 79 94 <Spinner />