[READ-ONLY] Mirror of https://github.com/bombshell-dev/rfd.
0

Configure Feed

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

refactor handle resolution

Nate Moore (May 11, 2026, 9:23 PM EDT) 836c6faa f8a6f361

+173 -19
+30 -15
packages/www/src/lib/atproto.ts
··· 10 10 } from '@atcute/identity-resolver'; 11 11 import type { ActorIdentifier } from '@atcute/lexicons/syntax'; 12 12 13 - const handleResolver = new CompositeHandleResolver({ 14 - strategy: 'race', 15 - methods: { 16 - dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }), 17 - http: new WellKnownHandleResolver(), 18 - }, 19 - }); 13 + import { resolveActorViaSlingshot } from './slingshot.ts'; 20 14 21 - const didDocumentResolver = new CompositeDidDocumentResolver({ 22 - methods: { 23 - plc: new PlcDidDocumentResolver(), 24 - web: new WebDidDocumentResolver(), 25 - }, 26 - }); 15 + let lazyLocalResolver: LocalActorResolver | undefined; 27 16 28 - const actorResolver = new LocalActorResolver({ handleResolver, didDocumentResolver }); 17 + function localResolver(): LocalActorResolver { 18 + if (!lazyLocalResolver) { 19 + const handleResolver = new CompositeHandleResolver({ 20 + strategy: 'race', 21 + methods: { 22 + dns: new DohJsonHandleResolver({ 23 + dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query', 24 + }), 25 + http: new WellKnownHandleResolver(), 26 + }, 27 + }); 28 + const didDocumentResolver = new CompositeDidDocumentResolver({ 29 + methods: { 30 + plc: new PlcDidDocumentResolver(), 31 + web: new WebDidDocumentResolver(), 32 + }, 33 + }); 34 + lazyLocalResolver = new LocalActorResolver({ handleResolver, didDocumentResolver }); 35 + } 36 + return lazyLocalResolver; 37 + } 29 38 30 39 export interface ResolvedActor { 31 40 did: string; ··· 34 43 } 35 44 36 45 export async function resolveActor(actor: ActorIdentifier): Promise<ResolvedActor> { 37 - const r = await actorResolver.resolve(actor); 46 + try { 47 + const mini = await resolveActorViaSlingshot(actor); 48 + if (mini) return mini; 49 + } catch (err) { 50 + console.warn('slingshot resolveMiniDoc failed, falling back to local resolver', err); 51 + } 52 + const r = await localResolver().resolve(actor); 38 53 return { did: r.did, handle: r.handle, pds: r.pds }; 39 54 } 40 55
+18 -1
packages/www/src/lib/pull.ts
··· 130 130 export interface CollectCommentsDeps { 131 131 listLinks?: (pullUri: string) => AsyncIterable<string>; 132 132 hydrate?: (atUri: string) => Promise<HydratedRecord | null>; 133 + resolveHandle?: (did: string) => Promise<string | undefined>; 133 134 } 134 135 135 136 export async function collectCommentsFor( ··· 145 146 path: '.pull', 146 147 })); 147 148 const hydrate = deps.hydrate ?? hydrateRecord; 149 + const resolveHandle = deps.resolveHandle ?? defaultResolveHandle(); 148 150 149 151 const out: PullView['comments'] = []; 150 152 for await (const commentUri of listLinks(pullUri)) { ··· 154 156 if (value.pull !== pullUri) continue; 155 157 const parsed = parseAtUri(hydrated.uri); 156 158 if (!parsed) continue; 159 + const handle = await resolveHandle(parsed.did); 157 160 out.push({ 158 161 uri: hydrated.uri, 159 162 cid: hydrated.cid, 160 163 body: value.body, 161 164 createdAt: value.createdAt, 162 - author: { did: parsed.did }, 165 + author: { did: parsed.did, handle }, 163 166 }); 164 167 } 165 168 out.sort((a, b) => a.createdAt.localeCompare(b.createdAt)); 166 169 return out; 167 170 } 171 + 172 + function defaultResolveHandle(): (did: string) => Promise<string | undefined> { 173 + const cache = new Map<string, Promise<string | undefined>>(); 174 + return (did) => { 175 + let promise = cache.get(did); 176 + if (!promise) { 177 + promise = resolveActor(did as ActorIdentifier) 178 + .then((r) => r.handle) 179 + .catch(() => undefined); 180 + cache.set(did, promise); 181 + } 182 + return promise; 183 + }; 184 + }
+26 -1
packages/www/src/lib/slingshot.ts
··· 8 8 value: unknown; 9 9 } 10 10 11 - export interface HydrateOptions { 11 + export interface ResolvedActorMini { 12 + did: string; 13 + handle: string; 14 + pds: string; 15 + } 16 + 17 + export interface SlingshotOptions { 12 18 fetch?: typeof fetch; 13 19 baseUrl?: string; 14 20 signal?: AbortSignal; 15 21 } 22 + 23 + export type HydrateOptions = SlingshotOptions; 16 24 17 25 export async function hydrateRecord( 18 26 atUri: string, ··· 36 44 const body = (await res.json()) as HydratedRecord; 37 45 return body; 38 46 } 47 + 48 + export async function resolveActorViaSlingshot( 49 + identifier: string, 50 + options: SlingshotOptions = {}, 51 + ): Promise<ResolvedActorMini | null> { 52 + const fetchImpl = options.fetch ?? globalThis.fetch; 53 + const base = options.baseUrl ?? SLINGSHOT_BASE_URL; 54 + const params = new URLSearchParams({ identifier }); 55 + const url = `${base}/xrpc/com.bad-example.identity.resolveMiniDoc?${params.toString()}`; 56 + const res = await fetchImpl(url, { signal: options.signal }); 57 + if (res.status === 400) return null; 58 + if (!res.ok) { 59 + throw new Error(`slingshot resolveMiniDoc failed: ${res.status} ${await res.text()}`); 60 + } 61 + const body = (await res.json()) as { did: string; handle: string; pds: string }; 62 + return { did: body.did, handle: body.handle, pds: body.pds }; 63 + }
+23 -2
packages/www/test/pull-comments.test.ts
··· 30 30 'at://did:plc:a/sh.tangled.repo.pull.comment/c1', 31 31 ]), 32 32 hydrate, 33 + resolveHandle: async () => undefined, 33 34 }); 34 35 35 36 expect(comments.map((c) => c.body)).toEqual(['first', 'second']); 36 - expect(comments[0].author).toEqual({ did: 'did:plc:a' }); 37 - expect(comments[1].author).toEqual({ did: 'did:plc:b' }); 37 + expect(comments[0].author).toEqual({ did: 'did:plc:a', handle: undefined }); 38 + expect(comments[1].author).toEqual({ did: 'did:plc:b', handle: undefined }); 39 + }); 40 + 41 + it('resolves handles via the injected resolver', async () => { 42 + const hydrate = vi.fn().mockResolvedValue({ 43 + uri: 'at://did:plc:a/sh.tangled.repo.pull.comment/c1', 44 + cid: 'bafy1', 45 + value: { pull: PULL_URI, body: 'hi', createdAt: '2026-05-10T00:00:00Z' }, 46 + }); 47 + const resolveHandle = vi.fn().mockResolvedValue('alice.example'); 48 + 49 + const comments = await collectCommentsFor(PULL_URI, { 50 + listLinks: () => asyncIter(['at://did:plc:a/sh.tangled.repo.pull.comment/c1']), 51 + hydrate, 52 + resolveHandle, 53 + }); 54 + 55 + expect(comments[0].author).toEqual({ did: 'did:plc:a', handle: 'alice.example' }); 56 + expect(resolveHandle).toHaveBeenCalledWith('did:plc:a'); 38 57 }); 39 58 40 59 it('skips comments whose pull field does not match the requested pull (defensive)', async () => { ··· 51 70 const comments = await collectCommentsFor(PULL_URI, { 52 71 listLinks: () => asyncIter(['at://did:plc:b/sh.tangled.repo.pull.comment/c1']), 53 72 hydrate, 73 + resolveHandle: async () => undefined, 54 74 }); 55 75 56 76 expect(comments).toEqual([]); ··· 62 82 const comments = await collectCommentsFor(PULL_URI, { 63 83 listLinks: () => asyncIter(['at://did:plc:b/sh.tangled.repo.pull.comment/gone']), 64 84 hydrate, 85 + resolveHandle: async () => undefined, 65 86 }); 66 87 67 88 expect(comments).toEqual([]);
+76
packages/www/test/resolve-actor.test.ts
··· 1 + import { describe, expect, it, vi } from 'vitest'; 2 + 3 + import { resolveActorViaSlingshot } from '../src/lib/slingshot.ts'; 4 + 5 + describe('resolveActorViaSlingshot', () => { 6 + it('returns did/handle/pds from a successful response', async () => { 7 + const fetchMock = vi.fn().mockResolvedValue( 8 + new Response( 9 + JSON.stringify({ 10 + did: 'did:plc:abc', 11 + handle: 'alice.example', 12 + pds: 'https://pds.example', 13 + signing_key: 'multibase...', 14 + }), 15 + { status: 200, headers: { 'content-type': 'application/json' } }, 16 + ), 17 + ); 18 + 19 + const actor = await resolveActorViaSlingshot('alice.example', { 20 + fetch: fetchMock as unknown as typeof fetch, 21 + }); 22 + 23 + expect(actor).toEqual({ 24 + did: 'did:plc:abc', 25 + handle: 'alice.example', 26 + pds: 'https://pds.example', 27 + }); 28 + const calledUrl = fetchMock.mock.calls[0][0] as string; 29 + expect(calledUrl).toContain( 30 + 'https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc', 31 + ); 32 + expect(calledUrl).toContain('identifier=alice.example'); 33 + }); 34 + 35 + it('url-encodes DID identifiers (with colons)', async () => { 36 + const fetchMock = vi.fn().mockResolvedValue( 37 + new Response( 38 + JSON.stringify({ 39 + did: 'did:plc:abc', 40 + handle: 'alice.example', 41 + pds: 'https://pds.example', 42 + signing_key: 'k', 43 + }), 44 + { status: 200, headers: { 'content-type': 'application/json' } }, 45 + ), 46 + ); 47 + 48 + await resolveActorViaSlingshot('did:plc:abc', { 49 + fetch: fetchMock as unknown as typeof fetch, 50 + }); 51 + 52 + expect(fetchMock.mock.calls[0][0] as string).toContain('identifier=did%3Aplc%3Aabc'); 53 + }); 54 + 55 + it('returns null on 400 (identity not resolved)', async () => { 56 + const fetchMock = vi.fn().mockResolvedValue( 57 + new Response(JSON.stringify({ error: 'HandleNotFound' }), { status: 400 }), 58 + ); 59 + 60 + const actor = await resolveActorViaSlingshot('missing.example', { 61 + fetch: fetchMock as unknown as typeof fetch, 62 + }); 63 + 64 + expect(actor).toBeNull(); 65 + }); 66 + 67 + it('throws on other non-2xx responses', async () => { 68 + const fetchMock = vi.fn().mockResolvedValue(new Response('boom', { status: 500 })); 69 + 70 + await expect( 71 + resolveActorViaSlingshot('alice.example', { 72 + fetch: fetchMock as unknown as typeof fetch, 73 + }), 74 + ).rejects.toThrow(/slingshot/i); 75 + }); 76 + });