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<boolean> => {
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<void>((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<boolean> => {
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);
}
};