A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
at main 4.4 kB view raw
1import { useEffect, useRef, useState } from "react"; 2import { useDidResolution } from "./useDidResolution"; 3import { usePdsEndpoint } from "./usePdsEndpoint"; 4import { useAtProto } from "../providers/AtProtoProvider"; 5 6/** 7 * Status returned by {@link useBlob} containing blob URL and metadata flags. 8 */ 9export interface UseBlobState { 10 /** Object URL pointing to the fetched blob, when available. */ 11 url?: string; 12 /** Indicates whether a fetch is in progress. */ 13 loading: boolean; 14 /** Error encountered while fetching the blob. */ 15 error?: Error; 16} 17 18/** 19 * Fetches a blob from the DID's PDS (resolving handles when needed), exposes it as an object URL, and cleans up on unmount. 20 * 21 * @param handleOrDid - Bluesky handle or DID whose PDS hosts the blob. 22 * @param cid - Content identifier for the desired blob. 23 * @returns {UseBlobState} Object containing the object URL, loading flag, and any error. 24 */ 25export function useBlob( 26 handleOrDid: string | undefined, 27 cid: string | undefined, 28): UseBlobState { 29 const { 30 did, 31 error: didError, 32 loading: didLoading, 33 } = useDidResolution(handleOrDid); 34 const { 35 endpoint, 36 error: endpointError, 37 loading: endpointLoading, 38 } = usePdsEndpoint(did); 39 const { blobCache } = useAtProto(); 40 const [state, setState] = useState<UseBlobState>({ 41 loading: !!(handleOrDid && cid), 42 }); 43 const objectUrlRef = useRef<string | undefined>(undefined); 44 45 useEffect( 46 () => () => { 47 if (objectUrlRef.current) { 48 URL.revokeObjectURL(objectUrlRef.current); 49 objectUrlRef.current = undefined; 50 } 51 }, 52 [], 53 ); 54 55 useEffect(() => { 56 let cancelled = false; 57 58 const clearObjectUrl = () => { 59 if (objectUrlRef.current) { 60 URL.revokeObjectURL(objectUrlRef.current); 61 objectUrlRef.current = undefined; 62 } 63 }; 64 65 if (!handleOrDid || !cid) { 66 clearObjectUrl(); 67 setState({ loading: false }); 68 return () => { 69 cancelled = true; 70 }; 71 } 72 73 if (didError) { 74 clearObjectUrl(); 75 setState({ loading: false, error: didError }); 76 return () => { 77 cancelled = true; 78 }; 79 } 80 81 if (endpointError) { 82 clearObjectUrl(); 83 setState({ loading: false, error: endpointError }); 84 return () => { 85 cancelled = true; 86 }; 87 } 88 89 if (didLoading || endpointLoading || !did || !endpoint) { 90 setState((prev) => ({ ...prev, loading: true, error: undefined })); 91 return () => { 92 cancelled = true; 93 }; 94 } 95 96 const cachedBlob = blobCache.get(did, cid); 97 if (cachedBlob) { 98 const nextUrl = URL.createObjectURL(cachedBlob); 99 const prevUrl = objectUrlRef.current; 100 objectUrlRef.current = nextUrl; 101 if (prevUrl) URL.revokeObjectURL(prevUrl); 102 setState({ url: nextUrl, loading: false }); 103 return () => { 104 cancelled = true; 105 }; 106 } 107 108 let controller: AbortController | undefined; 109 let release: (() => void) | undefined; 110 111 (async () => { 112 try { 113 setState((prev) => ({ 114 ...prev, 115 loading: true, 116 error: undefined, 117 })); 118 const ensureResult = blobCache.ensure(did, cid, () => { 119 controller = new AbortController(); 120 const promise = (async () => { 121 const res = await fetch( 122 `${endpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`, 123 { signal: controller?.signal }, 124 ); 125 if (!res.ok) 126 throw new Error( 127 `Blob fetch failed (${res.status})`, 128 ); 129 return res.blob(); 130 })(); 131 return { promise, abort: () => controller?.abort() }; 132 }); 133 release = ensureResult.release; 134 const blob = await ensureResult.promise; 135 const nextUrl = URL.createObjectURL(blob); 136 const prevUrl = objectUrlRef.current; 137 objectUrlRef.current = nextUrl; 138 if (prevUrl) URL.revokeObjectURL(prevUrl); 139 if (!cancelled) setState({ url: nextUrl, loading: false }); 140 } catch (e) { 141 const aborted = 142 (controller && controller.signal.aborted) || 143 (e instanceof DOMException && e.name === "AbortError"); 144 if (aborted) return; 145 clearObjectUrl(); 146 if (!cancelled) setState({ loading: false, error: e as Error }); 147 } 148 })(); 149 150 return () => { 151 cancelled = true; 152 release?.(); 153 if ( 154 controller && 155 controller.signal.aborted && 156 objectUrlRef.current 157 ) { 158 URL.revokeObjectURL(objectUrlRef.current); 159 objectUrlRef.current = undefined; 160 } 161 }; 162 }, [ 163 handleOrDid, 164 cid, 165 did, 166 endpoint, 167 didLoading, 168 endpointLoading, 169 didError, 170 endpointError, 171 blobCache, 172 ]); 173 174 return state; 175}