export type TerminalResult = | { status: 'done'; result: T } | { status: 'failed'; message: string }; export type SubmitResult = { status: 'submitted'; taskId: string } | TerminalResult; export type PollResult = { status: 'pending'; detail?: string } | TerminalResult; export interface PolledTaskTimeoutContext { label: string; taskId: string; attempts: number; intervalMs: number; elapsedMs: number; lastPendingDetail?: string; } export interface RunPolledTaskOptions { submit: () => Promise>; poll: (taskId: string) => Promise>; intervalMs: number; maxAttempts: number; label: string; formatTimeout?: (context: PolledTaskTimeoutContext) => string; } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function runPolledTask({ submit, poll, intervalMs, maxAttempts, label, formatTimeout, }: RunPolledTaskOptions): Promise { const submitted = await submit(); if (submitted.status === 'done') return submitted.result; if (submitted.status === 'failed') throw new Error(submitted.message); let attempts = 0; let lastPendingDetail: string | undefined; while (attempts < maxAttempts) { await delay(intervalMs); const result = await poll(submitted.taskId); attempts++; if (result.status === 'done') return result.result; if (result.status === 'failed') throw new Error(result.message); lastPendingDetail = result.detail; } const timeoutContext: PolledTaskTimeoutContext = { label, taskId: submitted.taskId, attempts, intervalMs, elapsedMs: attempts * intervalMs, lastPendingDetail, }; const message = formatTimeout ? formatTimeout(timeoutContext) : `${label} timed out after ${attempts} polls`; throw new Error(message); }