Live video on the AT Protocol
at eli/ingest-customization 101 lines 2.9 kB view raw
1import { AppBskyActorDefs } from "@atproto/api"; 2import { 3 createContext, 4 useCallback, 5 useContext, 6 useRef, 7 useState, 8} from "react"; 9import { useUnauthenticatedBlueskyAppViewAgent } from "../streamplace-store"; 10 11interface ProfileCacheContextValue { 12 profiles: Record<string, AppBskyActorDefs.ProfileViewDetailed>; 13 requestProfiles: (dids: string[]) => void; 14} 15 16export const ProfileCacheContext = 17 createContext<ProfileCacheContextValue | null>(null); 18 19export function ProfileCacheProvider({ 20 children, 21}: { 22 children: React.ReactNode; 23}) { 24 const agent = useUnauthenticatedBlueskyAppViewAgent(); 25 const [profiles, setProfiles] = useState< 26 Record<string, AppBskyActorDefs.ProfileViewDetailed> 27 >({}); 28 const agentRef = useRef(agent); 29 agentRef.current = agent; 30 const profilesRef = useRef(profiles); 31 profilesRef.current = profiles; 32 const inFlight = useRef<Set<string>>(new Set()); 33 const pending = useRef<Set<string>>(new Set()); 34 const timer = useRef<ReturnType<typeof setTimeout> | null>(null); 35 36 const flush = useCallback(() => { 37 timer.current = null; 38 if (!agentRef.current) return; 39 40 const toFetch = [...pending.current] 41 .filter((d) => !(d in profilesRef.current) && !inFlight.current.has(d)) 42 .slice(0, 25); 43 toFetch.forEach((d) => pending.current.delete(d)); 44 45 if (toFetch.length === 0) return; 46 47 // If there are more beyond the batch limit, schedule the remainder 48 if (pending.current.size > 0) { 49 timer.current = setTimeout(flush, 0); 50 } 51 52 toFetch.forEach((d) => inFlight.current.add(d)); 53 54 agentRef.current 55 .getProfiles({ actors: toFetch }) 56 .then((result) => { 57 const newProfiles: Record< 58 string, 59 AppBskyActorDefs.ProfileViewDetailed 60 > = {}; 61 result.data.profiles.forEach((p) => { 62 newProfiles[p.did] = p; 63 }); 64 setProfiles((prev) => ({ ...prev, ...newProfiles })); 65 }) 66 .catch((e) => { 67 console.error("Failed to fetch profiles", e); 68 }) 69 .finally(() => { 70 toFetch.forEach((d) => inFlight.current.delete(d)); 71 }); 72 }, []); 73 74 const requestProfiles = useCallback( 75 (dids: string[]) => { 76 const toQueue = dids.filter( 77 (d) => !(d in profilesRef.current) && !inFlight.current.has(d), 78 ); 79 if (toQueue.length === 0) return; 80 toQueue.forEach((d) => pending.current.add(d)); 81 if (!timer.current) { 82 timer.current = setTimeout(flush, 50); 83 } 84 }, 85 [flush], 86 ); 87 88 return ( 89 <ProfileCacheContext.Provider value={{ profiles, requestProfiles }}> 90 {children} 91 </ProfileCacheContext.Provider> 92 ); 93} 94 95export function useProfileCache(): ProfileCacheContextValue { 96 const ctx = useContext(ProfileCacheContext); 97 if (!ctx) { 98 throw new Error("useProfileCache must be used within ProfileCacheProvider"); 99 } 100 return ctx; 101}