Live video on the AT Protocol
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}