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.

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.
| Fact | Value | Source |
|---|---|---|
| Terminal statuses | completed, failed, canceled | docs.sume.com Jobs and results |
| Poll hint field | next_poll_after_seconds on the status payload | docs.sume.com Jobs and results |
| Sync wait cap | 30 seconds, an HTTP budget not a job duration | docs.sume.com Jobs and results |
| Client deadline for video | 20 minutes is reasonable | docs.sume.com Jobs and results |
| Node release with the fix | 26.10.0 (Current), 2026-09-22 | nodejs.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
- Node fetch with AbortSignal.timeout: poll a Sume job
A Node recipe: submit a Sume job with fetch, bound each call with AbortSignal.timeout, poll status_url, read the result. A local timeout does not cancel it.
- Open Graph image 1200x630 with an AI API: generate 16:9, then crop
Meta recommends og:image at 1200 x 630 and a 1.91:1 ratio. Sume has no 1.91:1 option, so generate 16:9 and crop with 12 lines of Pillow. Steps and limits.
- OpenAI structured outputs: 5000 properties, 10 levels, vs Sume
OpenAI caps strict schemas at 5000 properties and 10 nesting levels. Sume's output_schema uses the same numbers but counts enums and strings differently.
- OpenRouter output_modalities=video vs Sume /v1/videos/models
OpenRouter lists video models three ways. Sume has a /v1/videos/models endpoint with the same shape plus a catalog. Fields to read before you submit.
Written by Sume