type Task = { taskId: string; status: string; [key: string]: unknown }; /** A deadline stops this client; it does not cancel the server task. */ export async function pollTask( taskId: string, apiKey: string, options: { timeoutMs?: number; signal?: AbortSignal; fetch?: typeof fetch; now?: () => number; sleep?: (ms: number, signal: AbortSignal) => Promise; } = {}, ): Promise { const now = options.now ?? Date.now; const deadline = now() + (options.timeoutMs ?? 45 * 60_000); const request = options.fetch ?? fetch; const deadlineSignal = AbortSignal.timeout(options.timeoutMs ?? 45 * 60_000); const signal = options.signal ? AbortSignal.any([options.signal, deadlineSignal]) : deadlineSignal; const sleep = options.sleep ?? ((ms: number, signal: AbortSignal) => new Promise((resolve, reject) => { signal.throwIfAborted(); const abort = () => { clearTimeout(timer); reject(signal.reason); }; const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, ms); signal.addEventListener('abort', abort, { once: true }); })); for (let attempt = 0; now() < deadline; attempt++) { signal.throwIfAborted(); const response = await request(`https://api.omnapi.com/api/v1/tasks/${encodeURIComponent(taskId)}`, { headers: { 'x-api-key': apiKey }, signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]), }); let waitMs = Math.min(2000 * 2 ** Math.min(attempt, 4), 30_000); if (response.status === 429 || response.status === 503) { const retry = response.headers.get('Retry-After'); if (retry) { const seconds = Number(retry); const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(retry) - now(); if (Number.isFinite(delay)) waitMs = Math.max(waitMs, delay); } } else { if (!response.ok) throw new Error(`Task query HTTP ${response.status}; preserve taskId ${taskId}.`); const task = await response.json() as Task; if (typeof task.status !== 'string') throw new Error('Invalid task response; preserve the original receipt.'); if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(task.status)) return task; } if (now() + waitMs >= deadline) break; await sleep(waitMs, signal); } throw new Error(`Polling deadline reached; resume task ${taskId}, do not create a replacement.`); }