social components inlay.at
atproto components sdui
86
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 174 lines 5.0 kB view raw
1import { AtUri } from "@atproto/syntax"; 2import { LexResolver } from "@atproto/lex-resolver"; 3import { resolveDidToService } from "./resolve.ts"; 4import { cacheGet, cacheSet } from "./cache.ts"; 5import { getServiceJwt } from "./auth.ts"; 6import { type Resolver, MissingError } from "@inlay/render"; 7 8const lexResolver = new LexResolver({}); 9 10type CacheTag = { 11 $type: string; 12 uri?: string; 13 subject?: string; 14 from?: string; 15}; 16 17type CachePolicy = { 18 life?: string; 19 tags?: CacheTag[]; 20}; 21 22type ComponentResponse = { 23 node: unknown; 24 cache?: CachePolicy; 25}; 26 27const SLINGSHOT = "https://slingshot.microcosm.blue"; 28 29async function fetchRecordFromPds(uri: string): Promise<unknown | null> { 30 const key = `record:${uri}`; 31 const hit = await cacheGet(key); 32 if (hit !== undefined) return hit; 33 34 const parsed = new AtUri(uri); 35 const res = await fetch( 36 `${SLINGSHOT}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parsed.host)}&collection=${encodeURIComponent(parsed.collection)}&rkey=${encodeURIComponent(parsed.rkey)}` 37 ); 38 if (!res.ok) { 39 // Cache misses to avoid hammering PDS for records that don't exist. 40 // Uses shorter TTL than hits — the record might be created later. 41 await cacheSet(key, null, { life: "minutes" }); 42 return null; 43 } 44 45 const data = await res.json(); 46 const value = data.value as Record<string, unknown>; 47 await cacheSet(key, value, { 48 life: "hours", 49 tags: [{ $type: "at.inlay.defs#tagRecord", uri }], 50 }); 51 return value; 52} 53 54// --- XRPC --- 55 56async function callXrpc( 57 serviceUrl: string, 58 params: { 59 nsid: string; 60 type?: string; 61 body?: unknown; 62 params?: Record<string, string>; 63 }, 64 jwt?: string | null 65): Promise<unknown> { 66 const headers: Record<string, string> = {}; 67 if (jwt) headers["Authorization"] = `Bearer ${jwt}`; 68 69 if (params.type === "query") { 70 const qs = new URLSearchParams(); 71 if (params.params) { 72 for (const [k, v] of Object.entries(params.params)) { 73 if (v != null) qs.set(k, v); 74 } 75 } 76 const qsStr = qs.toString(); 77 const url = `${serviceUrl}/xrpc/${params.nsid}${qsStr ? `?${qsStr}` : ""}`; 78 const res = await fetch(url, { headers }); 79 if (!res.ok) { 80 const text = await res.text().catch(() => ""); 81 MissingError.rethrowFromResponse(text); 82 throw new Error( 83 `XRPC query failed (${params.nsid}): ${res.status} ${text}` 84 ); 85 } 86 return res.json(); 87 } 88 89 headers["Content-Type"] = "application/json"; 90 const url = `${serviceUrl}/xrpc/${params.nsid}`; 91 const res = await fetch(url, { 92 method: "POST", 93 headers, 94 body: JSON.stringify(params.body ?? {}), 95 }); 96 if (!res.ok) { 97 const text = await res.text().catch(() => ""); 98 MissingError.rethrowFromResponse(text); 99 throw new Error( 100 `XRPC procedure failed (${params.nsid}): ${res.status} ${text}` 101 ); 102 } 103 return res.json(); 104} 105 106export function createResolver(viewerDid?: string | null): Resolver { 107 return { 108 async fetchRecord(uri) { 109 return fetchRecordFromPds(uri); 110 }, 111 112 async resolve(dids, collection, rkey) { 113 const uris = dids.map((did) => `at://${did}/${collection}/${rkey}`); 114 const promises = uris.map((uri) => fetchRecordFromPds(uri)); 115 for (let i = 0; i < dids.length; i++) { 116 const record = await promises[i]; 117 if (record) return { did: dids[i], uri: uris[i] as any, record }; 118 } 119 return null; 120 }, 121 122 async xrpc(params) { 123 let serviceUrl: string; 124 try { 125 serviceUrl = await resolveDidToService(params.did, "#inlay"); 126 } catch { 127 throw new Error( 128 `XRPC resolve failed for ${params.nsid} (did=${params.did})` 129 ); 130 } 131 132 // Personalized: get service JWT, skip server cache 133 if (params.personalized && viewerDid) { 134 const jwt = await getServiceJwt(viewerDid, params.did, params.nsid); 135 return callXrpc(serviceUrl, params, jwt); 136 } 137 138 if (!params.componentUri || params.type === "query") { 139 return callXrpc(serviceUrl, params); 140 } 141 142 const key = `xrpc:${JSON.stringify(params)}`; 143 const hit = await cacheGet(key); 144 if (hit !== undefined) return hit; 145 146 const value = await callXrpc(serviceUrl, params); 147 const response = value as ComponentResponse; 148 149 const tags: CacheTag[] = [ 150 ...(response.cache?.tags ?? []), 151 { $type: "at.inlay.defs#tagRecord", uri: params.componentUri }, 152 ]; 153 const life = response.cache?.life ?? "hours"; 154 155 await cacheSet(key, value, { life, tags }); 156 return value; 157 }, 158 159 async resolveLexicon(nsid) { 160 const key = `lexicon:${nsid}`; 161 const hit = await cacheGet(key); 162 if (hit !== undefined) return hit; 163 164 try { 165 const { lexicon } = await lexResolver.get(nsid); 166 await cacheSet(key, lexicon, { life: "hours" }); 167 return lexicon; 168 } catch { 169 await cacheSet(key, null, { life: "hours" }); 170 return null; 171 } 172 }, 173 }; 174}