// AbortSignal/AbortController through fetch, differentially against Node's // undici: pre-aborted signals, mid-flight aborts, mid-stream aborts (the // pending read rejects), AbortSignal.timeout, AbortSignal.any, custom // abort reasons, and the no-abort quiet path — byte-exact output both // sides, so the error SHAPES (DOMException, name, message, Error // inheritance, legacy code) are pinned to Node's exactly. const out = (line) => process.stdout.write(line + "\n"); const show = (tag, e) => out(`${tag} dom=${e instanceof DOMException} err=${e instanceof Error} code=${e.code} ${e.name}: ${e.message}`); export function runAbort(base) { (async () => { // an already-aborted signal rejects before any request happens try { await fetch(base + "/text", { signal: AbortSignal.abort() }); out("pre no-throw"); } catch (e) { show("pre", e); } // a custom abort reason is carried verbatim try { await fetch(base + "/text", { signal: AbortSignal.abort("my custom reason") }); out("custom no-throw"); } catch (e) { out(`custom typeof=${typeof e} ${e}`); } // abort mid-flight, before any response arrives { const c = new AbortController(); const p = fetch(base + "/slow", { signal: c.signal }); c.abort(); try { await p; out("mid no-throw"); } catch (e) { show("mid", e); } } // abort mid-stream: the pending read rejects with the reason { const c = new AbortController(); const res = await fetch(base + "/drip", { signal: c.signal }); const reader = res.body.getReader(); const first = await reader.read(); out(`drip-first ${new TextDecoder().decode(first.value)}`); const pending = reader.read(); c.abort(); try { await pending; out("stream no-throw"); } catch (e) { show("stream", e); } } // AbortSignal.timeout fires while the transfer is in flight try { await fetch(base + "/slow", { signal: AbortSignal.timeout(100) }); out("timeout no-throw"); } catch (e) { show("timeout", e); } // AbortSignal.any follows its first-aborting member (the AI-SDK // combine-signals shape: a user controller plus a timeout) { const c = new AbortController(); const combined = AbortSignal.any([c.signal, AbortSignal.timeout(4000)]); const p = fetch(base + "/slow", { signal: combined }); c.abort(); try { await p; out("any no-throw"); } catch (e) { show("any", e); } } // a Request-carried signal aborts the fetch built from it { const c = new AbortController(); const req = new Request(base + "/slow", { signal: c.signal }); const p = fetch(req); c.abort(); try { await p; out("request no-throw"); } catch (e) { show("request", e); } } // the quiet path: a signal that never aborts changes nothing, and an // abort AFTER the body was consumed is a no-op { const c = new AbortController(); const res = await fetch(base + "/text", { signal: c.signal }); out(`quiet ${res.status} ${JSON.stringify(await res.text())}`); c.abort(); out(`quiet-after aborted=${c.signal.aborted} reason=${c.signal.reason.name}`); } // signal surface: state, listener order, once, throwIfAborted { const c = new AbortController(); const s = c.signal; out(`surface aborted=${s.aborted} reason=${s.reason}`); const seen = []; s.onabort = () => seen.push("onabort"); s.addEventListener("abort", () => seen.push("first")); const twice = () => seen.push("once"); s.addEventListener("abort", twice, { once: true }); c.abort(); c.abort(); // second abort is a no-op out(`surface fired=${seen.join(",")}`); try { s.throwIfAborted(); out("surface no-throw"); } catch (e) { show("surface-throw", e); } } out("done"); })().catch((e) => { out(`FATAL ${e}`); }); }