Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374import { 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<KnotOrigin, { at: number; capabilities: readonly Capability[] | null }>();const asking = new Map<KnotOrigin, Promise<readonly Capability[] | null>>();
const capabilitiesOf = (body: unknown): readonly Capability[] | null => { if (typeof body !== "object" || body === null) return null; const { capabilities } = body as Record<string, unknown>; if (!Array.isArray(capabilities)) return null; return capabilities.every((token) => typeof token === "string") ? capabilities.map(capabilityOf) : null;};
const describeKnot = async (knot: KnotOrigin): Promise<readonly Capability[] | null> => { 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<readonly Capability[] | null> => { 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<WriteTarget> => { 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<WriteTarget> => bobbinUrl ? writeTarget(createBobbinClient({ serviceUrl: bobbinUrl }), repoDid) : Promise.resolve({ kind: "pds" });