import { useEffect, useState } from "react"; import { useDidResolution } from "./useDidResolution"; import { usePdsEndpoint } from "./usePdsEndpoint"; import { createAtprotoClient } from "../utils/atproto-client"; /** * Identifier trio required to address an AT Protocol record. */ export interface AtProtoRecordKey { /** Repository DID (or handle prior to resolution) containing the record. */ did?: string; /** NSID collection in which the record resides. */ collection?: string; /** Record key string uniquely identifying the record within the collection. */ rkey?: string; } /** * Loading state returned by {@link useAtProtoRecord}. */ export interface AtProtoRecordState { /** Resolved record value when fetch succeeds. */ record?: T; /** Error thrown while loading, if any. */ error?: Error; /** Indicates whether the hook is in a loading state. */ loading: boolean; } /** * React hook that fetches a single AT Protocol record and tracks loading/error state. * * @param did - DID (or handle before resolution) that owns the record. * @param collection - NSID collection from which to fetch the record. * @param rkey - Record key identifying the record within the collection. * @returns {AtProtoRecordState} Object containing the resolved record, any error, and a loading flag. */ export function useAtProtoRecord({ did: handleOrDid, collection, rkey, }: AtProtoRecordKey): AtProtoRecordState { const { did, error: didError, loading: resolvingDid, } = useDidResolution(handleOrDid); const { endpoint, error: endpointError, loading: resolvingEndpoint, } = usePdsEndpoint(did); const [state, setState] = useState>({ loading: !!(handleOrDid && collection && rkey), }); useEffect(() => { let cancelled = false; const assignState = (next: Partial>) => { if (cancelled) return; setState((prev) => ({ ...prev, ...next })); }; if (!handleOrDid || !collection || !rkey) { assignState({ loading: false, record: undefined, error: undefined, }); return () => { cancelled = true; }; } if (didError) { assignState({ loading: false, error: didError }); return () => { cancelled = true; }; } if (endpointError) { assignState({ loading: false, error: endpointError }); return () => { cancelled = true; }; } if (resolvingDid || resolvingEndpoint || !did || !endpoint) { assignState({ loading: true, error: undefined }); return () => { cancelled = true; }; } assignState({ loading: true, error: undefined, record: undefined }); (async () => { try { const { rpc } = await createAtprotoClient({ service: endpoint, }); const res = await ( rpc as unknown as { get: ( nsid: string, opts: { params: { repo: string; collection: string; rkey: string; }; }, ) => Promise<{ ok: boolean; data: { value: T } }>; } ).get("com.atproto.repo.getRecord", { params: { repo: did, collection, rkey }, }); if (!res.ok) throw new Error("Failed to load record"); const record = (res.data as { value: T }).value; assignState({ record, loading: false }); } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); assignState({ error: err, loading: false }); } })(); return () => { cancelled = true; }; }, [ handleOrDid, did, endpoint, collection, rkey, resolvingDid, resolvingEndpoint, didError, endpointError, ]); return state; }