A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import type { DidDocument } from "@atcute/identity"; 2import { ServiceResolver } from "./atproto-client"; 3 4interface DidCacheEntry { 5 did: string; 6 handle?: string; 7 doc?: DidDocument; 8 pdsEndpoint?: string; 9 timestamp: number; 10} 11 12export interface DidCacheSnapshot { 13 did?: string; 14 handle?: string; 15 doc?: DidDocument; 16 pdsEndpoint?: string; 17} 18 19const toSnapshot = ( 20 entry: DidCacheEntry | undefined, 21): DidCacheSnapshot | undefined => { 22 if (!entry) return undefined; 23 const { did, handle, doc, pdsEndpoint } = entry; 24 return { did, handle, doc, pdsEndpoint }; 25}; 26 27const derivePdsEndpoint = ( 28 doc: DidDocument | undefined, 29): string | undefined => { 30 if (!doc?.service) return undefined; 31 const svc = doc.service.find( 32 (service) => service.type === "AtprotoPersonalDataServer", 33 ); 34 if (!svc) return undefined; 35 const endpoint = 36 typeof svc.serviceEndpoint === "string" 37 ? svc.serviceEndpoint 38 : undefined; 39 if (!endpoint) return undefined; 40 return endpoint.replace(/\/$/, ""); 41}; 42 43export class DidCache { 44 private byHandle = new Map<string, DidCacheEntry>(); 45 private byDid = new Map<string, DidCacheEntry>(); 46 private handlePromises = new Map<string, Promise<DidCacheSnapshot>>(); 47 private docPromises = new Map<string, Promise<DidCacheSnapshot>>(); 48 private pdsPromises = new Map<string, Promise<DidCacheSnapshot>>(); 49 50 getByHandle(handle: string | undefined): DidCacheSnapshot | undefined { 51 if (!handle) return undefined; 52 return toSnapshot(this.byHandle.get(handle.toLowerCase())); 53 } 54 55 getByDid(did: string | undefined): DidCacheSnapshot | undefined { 56 if (!did) return undefined; 57 return toSnapshot(this.byDid.get(did)); 58 } 59 60 memoize(entry: { 61 did: string; 62 handle?: string; 63 doc?: DidDocument; 64 pdsEndpoint?: string; 65 }): DidCacheSnapshot { 66 const did = entry.did; 67 const normalizedHandle = entry.handle?.toLowerCase(); 68 const existing = 69 this.byDid.get(did) ?? 70 (normalizedHandle 71 ? this.byHandle.get(normalizedHandle) 72 : undefined); 73 74 const doc = entry.doc ?? existing?.doc; 75 const handle = normalizedHandle ?? existing?.handle; 76 const pdsEndpoint = 77 entry.pdsEndpoint ?? 78 derivePdsEndpoint(doc) ?? 79 existing?.pdsEndpoint; 80 81 const merged: DidCacheEntry = { 82 did, 83 handle, 84 doc, 85 pdsEndpoint, 86 timestamp: Date.now(), 87 }; 88 89 this.byDid.set(did, merged); 90 if (handle) { 91 this.byHandle.set(handle, merged); 92 } 93 94 return toSnapshot(merged) as DidCacheSnapshot; 95 } 96 97 ensureHandle( 98 resolver: ServiceResolver, 99 handle: string, 100 ): Promise<DidCacheSnapshot> { 101 const normalized = handle.toLowerCase(); 102 const cached = this.getByHandle(normalized); 103 if (cached?.did) return Promise.resolve(cached); 104 const pending = this.handlePromises.get(normalized); 105 if (pending) return pending; 106 const promise = resolver 107 .resolveHandle(normalized) 108 .then((did) => this.memoize({ did, handle: normalized })) 109 .finally(() => { 110 this.handlePromises.delete(normalized); 111 }); 112 this.handlePromises.set(normalized, promise); 113 return promise; 114 } 115 116 ensureDidDoc( 117 resolver: ServiceResolver, 118 did: string, 119 ): Promise<DidCacheSnapshot> { 120 const cached = this.getByDid(did); 121 if (cached?.doc && cached.handle !== undefined) 122 return Promise.resolve(cached); 123 const pending = this.docPromises.get(did); 124 if (pending) return pending; 125 const promise = resolver 126 .resolveDidDoc(did) 127 .then((doc) => { 128 const aka = doc.alsoKnownAs?.find((a) => a.startsWith("at://")); 129 const handle = aka 130 ? aka.replace("at://", "").toLowerCase() 131 : cached?.handle; 132 return this.memoize({ did, handle, doc }); 133 }) 134 .finally(() => { 135 this.docPromises.delete(did); 136 }); 137 this.docPromises.set(did, promise); 138 return promise; 139 } 140 141 ensurePdsEndpoint( 142 resolver: ServiceResolver, 143 did: string, 144 ): Promise<DidCacheSnapshot> { 145 const cached = this.getByDid(did); 146 if (cached?.pdsEndpoint) return Promise.resolve(cached); 147 const pending = this.pdsPromises.get(did); 148 if (pending) return pending; 149 const promise = (async () => { 150 const docSnapshot = await this.ensureDidDoc(resolver, did).catch( 151 () => undefined, 152 ); 153 if (docSnapshot?.pdsEndpoint) return docSnapshot; 154 const endpoint = await resolver.pdsEndpointForDid(did); 155 return this.memoize({ did, pdsEndpoint: endpoint }); 156 })().finally(() => { 157 this.pdsPromises.delete(did); 158 }); 159 this.pdsPromises.set(did, promise); 160 return promise; 161 } 162} 163 164interface BlobCacheEntry { 165 blob: Blob; 166 timestamp: number; 167} 168 169interface InFlightBlobEntry { 170 promise: Promise<Blob>; 171 abort: () => void; 172 refCount: number; 173} 174 175interface EnsureResult { 176 promise: Promise<Blob>; 177 release: () => void; 178} 179 180export class BlobCache { 181 private store = new Map<string, BlobCacheEntry>(); 182 private inFlight = new Map<string, InFlightBlobEntry>(); 183 184 private key(did: string, cid: string): string { 185 return `${did}::${cid}`; 186 } 187 188 get(did?: string, cid?: string): Blob | undefined { 189 if (!did || !cid) return undefined; 190 return this.store.get(this.key(did, cid))?.blob; 191 } 192 193 set(did: string, cid: string, blob: Blob): void { 194 this.store.set(this.key(did, cid), { blob, timestamp: Date.now() }); 195 } 196 197 ensure( 198 did: string, 199 cid: string, 200 loader: () => { promise: Promise<Blob>; abort: () => void }, 201 ): EnsureResult { 202 const cached = this.get(did, cid); 203 if (cached) { 204 return { promise: Promise.resolve(cached), release: () => {} }; 205 } 206 207 const key = this.key(did, cid); 208 const existing = this.inFlight.get(key); 209 if (existing) { 210 existing.refCount += 1; 211 return { 212 promise: existing.promise, 213 release: () => this.release(key), 214 }; 215 } 216 217 const { promise, abort } = loader(); 218 const wrapped = promise.then((blob) => { 219 this.set(did, cid, blob); 220 return blob; 221 }); 222 223 const entry: InFlightBlobEntry = { 224 promise: wrapped, 225 abort, 226 refCount: 1, 227 }; 228 229 this.inFlight.set(key, entry); 230 231 wrapped 232 .catch(() => {}) 233 .finally(() => { 234 this.inFlight.delete(key); 235 }); 236 237 return { 238 promise: wrapped, 239 release: () => this.release(key), 240 }; 241 } 242 243 private release(key: string) { 244 const entry = this.inFlight.get(key); 245 if (!entry) return; 246 entry.refCount -= 1; 247 if (entry.refCount <= 0) { 248 this.inFlight.delete(key); 249 entry.abort(); 250 } 251 } 252}