import { TaskTimeout } from "./errors.js"; import { nowMs } from "./ids.js"; import { isTerminal, type Task } from "./models.js"; import type { TaskStore } from "./store/base.js"; export const DEFAULT_POLL_MS = 100; export const MAX_POLL_MS = 500; const GROWTH = 1.5; /** * Grow the polling interval towards the ceiling. * * wait() has no idea whether the task takes 50ms or an hour. Starting tight keeps * short tasks snappy; growing keeps a long wait from costing a read every 100ms * for its whole duration. The +1 keeps truncation from pinning tiny intervals: * Math.floor(1 * 1.5) === 1 would otherwise never grow past 1. */ export function nextPollMs(current: number, maxMs: number): number { return Math.min(maxMs, Math.max(current + 1, Math.floor(current * GROWTH))); } /** Poll get() until terminal or timeout. Returns the terminal Task (any status). * Throws TaskTimeout, leaving the task running. `pollMs` is the *first* interval; * it backs off towards `maxPollMs`. */ export async function pollWait( store: TaskStore, taskId: string, { timeoutMs, pollMs = DEFAULT_POLL_MS, maxPollMs = MAX_POLL_MS, }: { timeoutMs: number; pollMs?: number; maxPollMs?: number }, ): Promise { const deadline = nowMs() + timeoutMs; let interval = pollMs; for (;;) { const task = await store.get(taskId); if (task && isTerminal(task)) return task; const remaining = deadline - nowMs(); if (remaining <= 0) throw new TaskTimeout(taskId, { timeoutMs, task }); // A store with a push channel (Postgres) cuts the sleep short when the task // goes terminal; the re-get above stays the source of truth either way. await store.taskDoneWake(taskId, Math.min(interval, remaining)); interval = nextPollMs(interval, maxPollMs); } }