import { Result } from "better-result"; import type { ApiClientError, InternalApiClient } from "./api-client.ts"; import { ApiError, CancelledError, TimeoutError, VersionFailedError, } from "./errors.ts"; export type PollOptions = { targetStatus: "running" | "stopped"; timeoutSeconds: number; pollIntervalMs: number; signal?: AbortSignal; onStatusChange?(status: string): void; }; export type PollResult = { previewDomain: string; lastStatus: string; }; export type PollError = | CancelledError | TimeoutError | VersionFailedError | ApiClientError; /** * Polls a compute version until it reaches the target status, fails, or times out. * * Returns a Result with the preview domain on success or a typed error. */ export async function pollVersionStatus( api: InternalApiClient, versionId: string, options: PollOptions, ): Promise> { return Result.gen(async function* () { const deadline = Date.now() + options.timeoutSeconds * 1_000; let lastStatus = ""; while (Date.now() <= deadline) { if (options.signal?.aborted) { return Result.err(new CancelledError()); } const version = yield* Result.await( api.getVersion(versionId, options.signal), ); if (version.status !== lastStatus) { lastStatus = version.status; options.onStatusChange?.(version.status); } if (version.status === options.targetStatus) { if (options.targetStatus === "running" && !version.previewDomain) { return Result.err( new ApiError({ statusCode: 0, statusText: "", message: "Version reached running state without a previewDomain", traceHeaders: {}, }), ); } return Result.ok({ previewDomain: version.previewDomain ?? "", lastStatus: version.status, }); } if (version.status === "failed") { return Result.err(new VersionFailedError({ versionId })); } await sleep(options.pollIntervalMs, options.signal); } return Result.err( new TimeoutError({ versionId, elapsedMs: Date.now() - (deadline - options.timeoutSeconds * 1_000), lastStatus, }), ); }); } function sleep(ms: number, signal?: AbortSignal): Promise { return new Promise((resolve) => { if (signal?.aborted) { resolve(); return; } const onAbort = () => { clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); signal?.addEventListener("abort", onAbort, { once: true }); }); }