Something went wrong. Try again.
forked from tangled.org/core
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273import { createBobbinClient, type BobbinContext } from "#lib/api/client.ts";import { resolveRepoKnot } from "#lib/api/repo.ts";import { capabilityOf, type Capability, type Did, type KnotOrigin } from "#lib/api/syntax.ts";
export const SOCIAL_WRITES_CAPABILITY = capabilityOf("knot-social-writes");
const DESCRIBE_KNOT_NSID = "org.tangled.knot.describeKnot";export 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.status === 404) return []; 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 const readTarget = async ( ctx: BobbinContext, repoDid: Did, fosterUrl?: KnotOrigin | ""): Promise<KnotOrigin | undefined> => { const knot = await resolveRepoKnot(ctx, repoDid); if (!knot) return undefined; const capabilities = await knotCapabilities(knot); return capabilities === null || capabilities.includes(SOCIAL_WRITES_CAPABILITY) ? knot : fosterUrl || knot;};
export const readTargetFor = ( bobbinUrl: string | undefined, repoDid: Did, fosterUrl?: KnotOrigin | ""): Promise<KnotOrigin | undefined> => bobbinUrl ? readTarget(createBobbinClient({ serviceUrl: bobbinUrl }), repoDid, fosterUrl) : Promise.resolve(undefined);