Poll a Sume job with AbortSignal.any in Node 26.10

Node 26.10.0 fixes AbortSignal.any() propagation. Here is a Sume job poll loop with a hard deadline and a caller cancel, using next_poll_after_seconds.

5 min readSume
All posts

To poll a Sume job with a hard deadline and a caller-controlled cancel, build one signal with AbortSignal.any([AbortSignal.timeout(ms), callerSignal]), pass it to every fetch against GET /v1/jobs/{id}/status, and sleep for next_poll_after_seconds between reads. The loop below is about 20 lines, has no dependencies, and stops on the terminal boolean.

The reason to revisit this pattern now: Node.js 26.10.0, released 2026-09-22, lists a fix to AbortSignal.any() abort propagation among its changes. If your poller merged a deadline with a user cancel, upgrading is worth a test run. Node 26 is a Current line, and the same release page names 22.23.3 as the current LTS, so check which line you deploy before assuming the fix is in production.

What do the docs say the loop must do?

Sume's jobs docs describe the client-side wait: submit with mode: "async", read the status until terminal is true, then read the result. The wait lives in your client, and a client timeout does not cancel the job; it keeps running and still bills.

Facts used here, read 2026-10-02
FactValueSource
Terminal statusescompleted, failed, canceleddocs.sume.com Jobs and results
Poll hint fieldnext_poll_after_seconds on the status payloaddocs.sume.com Jobs and results
Sync wait cap30 seconds, an HTTP budget not a job durationdocs.sume.com Jobs and results
Client deadline for video20 minutes is reasonabledocs.sume.com Jobs and results
Node release with the fix26.10.0 (Current), 2026-09-22nodejs.org release notes

The poll loop

Run it as an ES module on Node 20.3 or newer, where AbortSignal.any exists. It throws the signal's reason when the deadline or the caller aborts, so you can tell a TimeoutError from an AbortError.

const BASE = process.env.SUME_BASE_URL ?? "https://api.sume.com";
const headers = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };

export async function pollJob(jobId, { deadlineMs = 20 * 60_000, signal } = {}) {
  const stop = AbortSignal.any([
    AbortSignal.timeout(deadlineMs),
    ...(signal ? [signal] : []),
  ]);
  for (;;) {
    const res = await fetch(`${BASE}/v1/jobs/${jobId}/status`, { headers, signal: stop });
    if (!res.ok) throw new Error(`status read failed: ${res.status}`);
    const { data } = await res.json();
    if (data.terminal) return data;
    const wait = Math.max(2, data.next_poll_after_seconds ?? 2) * 1000;
    await new Promise((resolve, reject) => {
      const t = setTimeout(resolve, wait);
      stop.addEventListener("abort", () => { clearTimeout(t); reject(stop.reason); }, { once: true });
    });
  }
}

Using it

Submit with an Idempotency-Key, keep the returned request_id, and hand it to pollJob. When the loop throws a TimeoutError, store the job id and read it later; do not submit again.

Read data.sume_status for the outcome. On completed, fetch GET /v1/jobs/{id}/result. On failed or canceled, read the job record instead, because /result answers 409 job_not_completed for those.

Limits

This is a plain fetch loop. It does not retry a failed status read, so one 429 or 5xx ends it; the TypeScript SDK's waitForJob tolerates transient read failures and is the better default if you can install a package. I ran the loop against a local mock that returns the documented status shape, not against production, and on Node 22 rather than 26.10, so the propagation fix itself is cited from the release notes and not tested here.

The deadline stops your watching only. A job past the point of no return still completes and still bills, and cancel only works before generation starts.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume