import { createBobbinClient, type BobbinContext, type XrpcRequestInit } from "$lib/api/client"; import { resolveRepoKnot } from "$lib/api/repo"; import { capabilityOf, knotOf, type Capability, type Did, type KnotOrigin } from "$lib/api/syntax"; export const SOCIAL_WRITES_CAPABILITY = capabilityOf("knot-social-writes"); const DESCRIBE_KNOT_NSID = "org.tangled.knot.describeKnot"; const CAPABILITY_TTL_MS = 5 * 60_000; const FAILED_PROBE_TTL_MS = 30_000; const PROBE_TIMEOUT_MS = 10_000; const probes = new Map(); const asking = new Map>(); const capabilitiesOf = (body: unknown): readonly Capability[] | null => { if (typeof body !== "object" || body === null) return null; const { capabilities } = body as Record; if (!Array.isArray(capabilities)) return null; return capabilities.every((token) => typeof token === "string") ? capabilities.map(capabilityOf) : null; }; const describeKnot = async (knot: KnotOrigin): Promise => { const response = await fetch(`https://${knot}/xrpc/${DESCRIBE_KNOT_NSID}`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) }); if (!response.ok) return null; return capabilitiesOf(await response.json().catch(() => null)); }; export const knotCapabilities = async ( knot: KnotOrigin, now: () => number = Date.now ): Promise => { const cached = probes.get(knot); const ttl = cached?.capabilities === null ? FAILED_PROBE_TTL_MS : CAPABILITY_TTL_MS; if (cached && now() - cached.at < ttl) return cached.capabilities; const pending = asking.get(knot); if (pending) return pending; const probe = describeKnot(knot) .catch(() => null) .finally(() => asking.delete(knot)); asking.set(knot, probe); const capabilities = await probe; probes.set(knot, { at: now(), capabilities }); return capabilities; }; export type WriteTarget = { kind: "pds" } | { kind: "knot"; knot: KnotOrigin; repoDid: Did }; export const writeTarget = async ( ctx: BobbinContext, repoDid: Did, init?: XrpcRequestInit ): Promise => { const host = await resolveRepoKnot(ctx, repoDid, init); if (!host) return { kind: "pds" }; const knot = knotOf(host); const capabilities = await knotCapabilities(knot); return capabilities?.includes(SOCIAL_WRITES_CAPABILITY) ? { kind: "knot", knot, repoDid } : { kind: "pds" }; }; export const writeTargetFor = ( bobbinUrl: string | undefined, repoDid: Did ): Promise => bobbinUrl ? writeTarget(createBobbinClient({ serviceUrl: bobbinUrl }), repoDid) : Promise.resolve({ kind: "pds" });