import type { BobbinContext } from "#lib/api/client.ts"; import { fetchMiniDoc, type MiniDoc } from "#lib/api/identity.ts"; export interface MigrationConfig { liarPdsUrl: string; liarMock: boolean; } const originOf = (url: string): string | null => { try { return new URL(url).origin; } catch { return null; } }; export const sameOrigin = (candidate: string | undefined, expectedUrl: string): boolean => { const expected = originOf(expectedUrl); return expected !== null && candidate !== undefined && originOf(candidate) === expected; }; // not cached, it has to flip as soon as hydrant unmarks the account export const migrationPending = async ( ctx: BobbinContext, did: string, config: MigrationConfig ): Promise => { if (!config.liarPdsUrl) return false; let doc: MiniDoc = await fetchMiniDoc(ctx, did); if (config.liarMock) { const { mockHydrantDoc } = await import("#lib/migration/mock.ts"); doc = await mockHydrantDoc(doc, config.liarPdsUrl, ctx.fetch); } return sameOrigin(doc.pds, config.liarPdsUrl); }; export const sleep = (ms: number, signal?: AbortSignal) => new Promise((done, fail) => { if (signal?.aborted) return fail(signal.reason); const onAbort = () => { clearTimeout(timer); fail(signal?.reason); }; const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); done(); }, ms); signal?.addEventListener("abort", onAbort, { once: true }); }); // hydrant may lag the unmark, so this polls the doc export const waitForMigration = async ( ctx: BobbinContext, did: string, config: MigrationConfig, { intervalMs = 2000, timeoutMs = 60_000, signal }: { intervalMs?: number; timeoutMs?: number; signal?: AbortSignal } = {} ): Promise => { const deadline = Date.now() + timeoutMs; for (;;) { // the records are already moved, so a failed lookup is just another poll const pending = await migrationPending(ctx, did, config).catch(() => true); if (!pending) return true; const left = deadline - Date.now(); if (left <= 0) return false; await sleep(Math.min(intervalMs, left), signal); } };