web components for a integratabtle atproto based guestbook

async fetch profiles

nekomimi.pet 7befa943 f4580322

verified
+75 -33
+75 -33
lib/guestbook-display.ts
··· 315 315 } 316 316 317 317 const data: ConstellationBacklinksResponse = await response.json(); 318 - 318 + 319 319 console.log('Constellation response:', data); 320 320 321 321 // fetch actual records ··· 323 323 console.warn('No records found in response'); 324 324 this.signatures = []; 325 325 } else { 326 - // fetch each record from the repository 327 - const signatures: GuestbookSignature[] = []; 328 - 329 - for (const record of data.records) { 326 + // Fetch all records in parallel 327 + const recordPromises = data.records.map(async (record) => { 330 328 try { 331 329 const recordUrl = new URL('/xrpc/com.atproto.repo.getRecord', 'https://slingshot.wisp.place'); 332 330 recordUrl.searchParams.set('repo', record.did); 333 331 recordUrl.searchParams.set('collection', record.collection); 334 332 recordUrl.searchParams.set('rkey', record.rkey); 335 - 333 + 336 334 const recordResponse = await fetch(recordUrl.toString()); 337 335 if (!recordResponse.ok) { 338 336 console.warn(`Failed to fetch record ${record.did}/${record.collection}/${record.rkey}`); 339 - continue; 337 + return null; 340 338 } 341 - 339 + 342 340 const recordData = await recordResponse.json(); 343 - 341 + 344 342 // validate the record 345 343 if ( 346 344 recordData.value && ··· 348 346 typeof recordData.value.message === 'string' && 349 347 typeof recordData.value.createdAt === 'string' 350 348 ) { 351 - // Fetch the handle for this author 352 - let authorHandle: string | undefined; 353 - try { 354 - const profileResponse = await fetch( 355 - `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${record.did}` 356 - ); 357 - if (profileResponse.ok) { 358 - const profileData = await profileResponse.json(); 359 - authorHandle = profileData.handle; 360 - } 361 - } catch (err) { 362 - console.warn(`Failed to fetch handle for ${record.did}:`, err); 363 - } 364 - 365 - signatures.push({ 349 + return { 366 350 uri: recordData.uri, 367 351 cid: recordData.cid, 368 352 value: recordData.value, 369 353 author: record.did, 370 - authorHandle: authorHandle, 371 - }); 354 + authorHandle: undefined, 355 + } as GuestbookSignature; 372 356 } 373 357 } catch (err) { 374 358 console.warn(`Error fetching record ${record.did}/${record.collection}/${record.rkey}:`, err); 375 359 } 376 - } 377 - 378 - // sort by creation time, newest first 379 - this.signatures = signatures.sort((a, b) => { 360 + return null; 361 + }); 362 + 363 + const results = await Promise.all(recordPromises); 364 + const validSignatures = results.filter((sig): sig is GuestbookSignature => sig !== null); 365 + 366 + // Sort once after collecting all signatures 367 + validSignatures.sort((a, b) => { 380 368 return new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime(); 381 369 }); 382 - } 383 370 371 + this.signatures = validSignatures; 384 372 this.loading = false; 385 373 this.updateContent(); 386 374 375 + // Batch fetch profiles asynchronously 376 + if (validSignatures.length > 0) { 377 + const uniqueDids = Array.from(new Set(validSignatures.map(sig => sig.author))); 378 + 379 + // Batch fetch profiles up to 25 at a time (API limit) 380 + const profilePromises = []; 381 + for (let i = 0; i < uniqueDids.length; i += 25) { 382 + const batch = uniqueDids.slice(i, i + 25); 383 + 384 + const profileUrl = new URL('/xrpc/app.bsky.actor.getProfiles', 'https://public.api.bsky.app'); 385 + batch.forEach(d => profileUrl.searchParams.append('actors', d)); 386 + 387 + profilePromises.push( 388 + fetch(profileUrl.toString()) 389 + .then(profileResponse => profileResponse.ok ? profileResponse.json() : null) 390 + .then(profilesData => { 391 + if (profilesData?.profiles && Array.isArray(profilesData.profiles)) { 392 + const handles = new Map<string, string>(); 393 + profilesData.profiles.forEach((profile: any) => { 394 + if (profile.handle) { 395 + handles.set(profile.did, profile.handle); 396 + } 397 + }); 398 + return handles; 399 + } 400 + return new Map<string, string>(); 401 + }) 402 + .catch((err) => { 403 + console.warn('Failed to fetch profile batch:', err); 404 + return new Map<string, string>(); 405 + }) 406 + ); 407 + } 408 + 409 + // Wait for all profile batches, then update once 410 + const handleMaps = await Promise.all(profilePromises); 411 + const allHandles = new Map<string, string>(); 412 + handleMaps.forEach(map => { 413 + map.forEach((handle, did) => allHandles.set(did, handle)); 414 + }); 415 + 416 + if (allHandles.size > 0) { 417 + this.signatures = this.signatures.map(sig => { 418 + const handle = allHandles.get(sig.author); 419 + return handle ? { ...sig, authorHandle: handle } : sig; 420 + }); 421 + this.updateContent(); 422 + } 423 + } 424 + } 425 + 387 426 } catch (error) { 388 427 console.error('Error fetching signatures:', error); 389 428 this.error = error instanceof Error ? error.message : 'Unknown error occurred'; ··· 403 442 } 404 443 405 444 private shortenDid(did: string): string { 406 - if (did.startsWith('did:plc:')) { 407 - return `${did.slice(0, 16)}...${did.slice(-4)}`; 445 + if (did.startsWith('did:')) { 446 + const afterPrefix = did.indexOf(':', 4); 447 + if (afterPrefix !== -1) { 448 + return `${did.slice(0, afterPrefix + 9)}...`; 449 + } 408 450 } 409 451 return did; 410 452 }