import { Result } from "better-result"; import type { ApiClientError, InternalApiClient } from "./api-client.ts"; import { ApiError, CancelledError, DeploymentFailedError, TimeoutError, } 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 | DeploymentFailedError | ApiClientError; /** * Polls a deployment 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 pollDeploymentStatus( api: InternalApiClient, deploymentId: 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 deployment = yield* Result.await( api.getDeployment(deploymentId, options.signal), ); if (deployment.status !== lastStatus) { lastStatus = deployment.status; options.onStatusChange?.(deployment.status); } if (deployment.status === options.targetStatus) { if (options.targetStatus === "running" && !deployment.previewDomain) { return Result.err( new ApiError({ statusCode: 0, statusText: "", message: "Deployment reached running state without a previewDomain", traceHeaders: {}, }), ); } return Result.ok({ previewDomain: deployment.previewDomain ?? "", lastStatus: deployment.status, }); } if (deployment.status === "failed") { return Result.err(new DeploymentFailedError({ deploymentId })); } await sleep(options.pollIntervalMs, options.signal); } return Result.err( new TimeoutError({ deploymentId, 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 }); }); }