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; }