A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
at main 4.6 kB view raw
1import { useEffect, useState, useCallback, useRef } from "react"; 2 3/** 4 * Individual backlink record returned by Microcosm Constellation. 5 */ 6export interface BacklinkRecord { 7 /** DID of the author who created the backlink. */ 8 did: string; 9 /** Collection type of the backlink record (e.g., "sh.tangled.feed.star"). */ 10 collection: string; 11 /** Record key of the backlink. */ 12 rkey: string; 13} 14 15/** 16 * Response from Microcosm Constellation API. 17 */ 18export interface BacklinksResponse { 19 /** Total count of backlinks. */ 20 total: number; 21 /** Array of backlink records. */ 22 records: BacklinkRecord[]; 23 /** Cursor for pagination (optional). */ 24 cursor?: string; 25} 26 27/** 28 * Parameters for fetching backlinks. 29 */ 30export interface UseBacklinksParams { 31 /** The AT-URI subject to get backlinks for (e.g., "at://did:plc:xxx/sh.tangled.repo/yyy"). */ 32 subject: string; 33 /** The source collection and path (e.g., "sh.tangled.feed.star:subject"). */ 34 source: string; 35 /** Maximum number of results to fetch (default: 16, max: 100). */ 36 limit?: number; 37 /** Base URL for the Microcosm Constellation API. */ 38 constellationBaseUrl?: string; 39 /** Whether to automatically fetch backlinks on mount. */ 40 enabled?: boolean; 41} 42 43const DEFAULT_CONSTELLATION = "https://constellation.microcosm.blue"; 44 45/** 46 * Hook to fetch backlinks from Microcosm Constellation API. 47 * 48 * Backlinks are records that reference another record. For example, 49 * `sh.tangled.feed.star` records are backlinks to `sh.tangled.repo` records, 50 * representing users who have starred a repository. 51 * 52 * @param params - Configuration for fetching backlinks 53 * @returns Object containing backlinks data, loading state, error, and refetch function 54 * 55 * @example 56 * ```tsx 57 * const { backlinks, loading, error, count } = useBacklinks({ 58 * subject: "at://did:plc:example/sh.tangled.repo/3k2aexample", 59 * source: "sh.tangled.feed.star:subject", 60 * }); 61 * ``` 62 */ 63export function useBacklinks({ 64 subject, 65 source, 66 limit = 16, 67 constellationBaseUrl = DEFAULT_CONSTELLATION, 68 enabled = true, 69}: UseBacklinksParams) { 70 const [backlinks, setBacklinks] = useState<BacklinkRecord[]>([]); 71 const [total, setTotal] = useState(0); 72 const [loading, setLoading] = useState(false); 73 const [error, setError] = useState<Error | undefined>(undefined); 74 const [cursor, setCursor] = useState<string | undefined>(undefined); 75 const abortControllerRef = useRef<AbortController | null>(null); 76 77 const fetchBacklinks = useCallback( 78 async (signal?: AbortSignal) => { 79 if (!subject || !source || !enabled) return; 80 81 try { 82 setLoading(true); 83 setError(undefined); 84 85 const baseUrl = constellationBaseUrl.endsWith("/") 86 ? constellationBaseUrl.slice(0, -1) 87 : constellationBaseUrl; 88 89 const params = new URLSearchParams({ 90 subject: subject, 91 source: source, 92 limit: limit.toString(), 93 }); 94 95 const url = `${baseUrl}/xrpc/blue.microcosm.links.getBacklinks?${params}`; 96 97 const response = await fetch(url, { signal }); 98 99 if (!response.ok) { 100 throw new Error( 101 `Failed to fetch backlinks: ${response.status} ${response.statusText}`, 102 ); 103 } 104 105 const data: BacklinksResponse = await response.json(); 106 setBacklinks(data.records || []); 107 setTotal(data.total || 0); 108 setCursor(data.cursor); 109 } catch (err) { 110 if (err instanceof Error && err.name === "AbortError") { 111 // Ignore abort errors 112 return; 113 } 114 setError( 115 err instanceof Error ? err : new Error("Unknown error fetching backlinks"), 116 ); 117 } finally { 118 setLoading(false); 119 } 120 }, 121 [subject, source, limit, constellationBaseUrl, enabled], 122 ); 123 124 const refetch = useCallback(() => { 125 // Abort any in-flight request 126 if (abortControllerRef.current) { 127 abortControllerRef.current.abort(); 128 } 129 130 const controller = new AbortController(); 131 abortControllerRef.current = controller; 132 fetchBacklinks(controller.signal); 133 }, [fetchBacklinks]); 134 135 useEffect(() => { 136 if (!enabled) return; 137 138 const controller = new AbortController(); 139 abortControllerRef.current = controller; 140 fetchBacklinks(controller.signal); 141 142 return () => { 143 controller.abort(); 144 }; 145 }, [fetchBacklinks, enabled]); 146 147 return { 148 /** Array of backlink records. */ 149 backlinks, 150 /** Whether backlinks are currently being fetched. */ 151 loading, 152 /** Error if fetch failed. */ 153 error, 154 /** Pagination cursor (not yet implemented for pagination). */ 155 cursor, 156 /** Total count of backlinks from the API. */ 157 total, 158 /** Total count of backlinks (alias for total). */ 159 count: total, 160 /** Function to manually refetch backlinks. */ 161 refetch, 162 }; 163}