feat(snapshot): relay-loader (kind:10002 + event-fetch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
10cb0d947d
commit
d8a29ca389
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Relay } from 'applesauce-relay'
|
||||||
|
import { firstValueFrom, timeout } from 'rxjs'
|
||||||
|
import type { SignedEvent } from './types.ts'
|
||||||
|
|
||||||
|
export type RelayListLoader = (
|
||||||
|
bootstrapRelay: string,
|
||||||
|
authorPubkey: string,
|
||||||
|
) => Promise<SignedEvent | undefined>
|
||||||
|
|
||||||
|
export const FALLBACK_READ_RELAYS = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relay.tchncs.de',
|
||||||
|
'wss://relay.edufeed.org',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function extractReadRelays(kind10002: SignedEvent): string[] {
|
||||||
|
const out: string[] = []
|
||||||
|
for (const tag of kind10002.tags) {
|
||||||
|
if (tag[0] !== 'r' || !tag[1]) continue
|
||||||
|
const marker = tag[2]
|
||||||
|
if (marker === 'write') continue
|
||||||
|
out.push(tag[1])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultRelayListLoader: RelayListLoader = async (bootstrap, pubkey) => {
|
||||||
|
try {
|
||||||
|
const relay = new Relay(bootstrap)
|
||||||
|
const ev = await firstValueFrom(
|
||||||
|
relay.request({ kinds: [10002], authors: [pubkey], limit: 1 })
|
||||||
|
.pipe(timeout({ first: 5_000 })),
|
||||||
|
)
|
||||||
|
return ev as SignedEvent
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadReadRelays(
|
||||||
|
bootstrapRelay: string,
|
||||||
|
authorPubkey: string,
|
||||||
|
loader: RelayListLoader = defaultRelayListLoader,
|
||||||
|
fallback: string[] = FALLBACK_READ_RELAYS,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const ev = await loader(bootstrapRelay, authorPubkey)
|
||||||
|
if (!ev) return fallback
|
||||||
|
const list = extractReadRelays(ev)
|
||||||
|
return list.length > 0 ? list : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchEventsResult {
|
||||||
|
events: SignedEvent[]
|
||||||
|
responded: string[]
|
||||||
|
queried: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventFetcher = (relay: string, pubkey: string) => Promise<SignedEvent[]>
|
||||||
|
|
||||||
|
export const defaultEventFetcher: EventFetcher = async (relay, pubkey) => {
|
||||||
|
const out: SignedEvent[] = []
|
||||||
|
const r = new Relay(relay)
|
||||||
|
return await new Promise<SignedEvent[]>((resolve) => {
|
||||||
|
const sub = r.request({ kinds: [30023, 5], authors: [pubkey] })
|
||||||
|
.pipe(timeout({ first: 10_000 }))
|
||||||
|
.subscribe({
|
||||||
|
next: (ev) => out.push(ev as SignedEvent),
|
||||||
|
error: () => resolve(out),
|
||||||
|
complete: () => resolve(out),
|
||||||
|
})
|
||||||
|
setTimeout(() => sub.unsubscribe(), 11_000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEvents(
|
||||||
|
relays: string[],
|
||||||
|
authorPubkey: string,
|
||||||
|
fetcher: EventFetcher = defaultEventFetcher,
|
||||||
|
): Promise<FetchEventsResult> {
|
||||||
|
const results = await Promise.all(
|
||||||
|
relays.map(async (url) => {
|
||||||
|
try {
|
||||||
|
const events = await fetcher(url, authorPubkey)
|
||||||
|
return { url, ok: true as const, events }
|
||||||
|
} catch {
|
||||||
|
return { url, ok: false as const, events: [] as SignedEvent[] }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const events: SignedEvent[] = []
|
||||||
|
for (const r of results) events.push(...r.events)
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
responded: results.filter((r) => r.ok).map((r) => r.url),
|
||||||
|
queried: relays,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { assertEquals } from '@std/assert'
|
||||||
|
import { extractReadRelays, type RelayListLoader, loadReadRelays } from '../src/core/relays.ts'
|
||||||
|
import type { SignedEvent } from '../src/core/types.ts'
|
||||||
|
|
||||||
|
const KIND_10002: SignedEvent = {
|
||||||
|
id: 'r', pubkey: 'P', created_at: 1, kind: 10002, sig: 's', content: '',
|
||||||
|
tags: [
|
||||||
|
['r', 'wss://relay.damus.io'],
|
||||||
|
['r', 'wss://nos.lol', 'read'],
|
||||||
|
['r', 'wss://relay.write-only.example', 'write'],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test('extractReadRelays: ohne marker = read+write, "read" = read, "write" = nicht', () => {
|
||||||
|
assertEquals(extractReadRelays(KIND_10002), [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
Deno.test('loadReadRelays: nutzt fallback wenn kein kind:10002', async () => {
|
||||||
|
const loader: RelayListLoader = async () => undefined
|
||||||
|
const relays = await loadReadRelays('wss://bootstrap', 'P', loader, [
|
||||||
|
'wss://fallback1', 'wss://fallback2',
|
||||||
|
])
|
||||||
|
assertEquals(relays, ['wss://fallback1', 'wss://fallback2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
Deno.test('loadReadRelays: nutzt kind:10002 wenn vorhanden', async () => {
|
||||||
|
const loader: RelayListLoader = async () => KIND_10002
|
||||||
|
const relays = await loadReadRelays('wss://bootstrap', 'P', loader, ['wss://fallback'])
|
||||||
|
assertEquals(relays, ['wss://relay.damus.io', 'wss://nos.lol'])
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue