Skip to content

globals: complete AbortSignal static helpers and abort lifecycle #2582

Description

@andrewtdiz

Summary

Node's global AbortController / AbortSignal surface is broader than Perry's current partial implementation. Current Node docs cover:

  • abortController.abort([reason])
  • AbortSignal.abort([reason])
  • AbortSignal.timeout(delay)
  • AbortSignal.any(signals)
  • abortSignal.reason
  • abortSignal.onabort
  • abortSignal.throwIfAborted()

Docs:

Perry origin/main implements enough for new AbortController(), controller.abort(reason), simple controller.signal.addEventListener('abort', cb), and an AbortSignal.timeout() shape stub, but it does not complete the Node lifecycle/static helper behavior.

Node behavior

Local Node probe with node v25.9.0:

AbortSignal static
abort,any,length,name,prototype,timeout
AbortSignal proto
aborted,constructor,onabort,reason,throwIfAborted
AbortController proto
abort,constructor,signal
initial false undefined
event reason boom
after true boom 1
throwIfAborted true boom
abort static true x
timeout initial false undefined
any initial false
any after true y

Probe script:

console.log('AbortSignal static');
console.log(Object.getOwnPropertyNames(AbortSignal).sort().join(','));
console.log('AbortSignal proto');
console.log(Object.getOwnPropertyNames(AbortSignal.prototype).sort().join(','));
console.log('AbortController proto');
console.log(Object.getOwnPropertyNames(AbortController.prototype).sort().join(','));

const ac = new AbortController();
console.log('initial', ac.signal.aborted, ac.signal.reason);
let seen = 0;
ac.signal.addEventListener('abort', () => {
  seen++;
  console.log('event reason', ac.signal.reason?.message || ac.signal.reason);
});
ac.abort(new Error('boom'));
console.log('after', ac.signal.aborted, ac.signal.reason?.message, seen);

try {
  ac.signal.throwIfAborted();
} catch (err) {
  console.log('throwIfAborted', err === ac.signal.reason, err.message);
}

const s1 = AbortSignal.abort('x');
console.log('abort static', s1.aborted, s1.reason);

const t = AbortSignal.timeout(5);
console.log('timeout initial', t.aborted, typeof t.reason);

const a = new AbortController();
const any = AbortSignal.any([a.signal]);
console.log('any initial', any.aborted);
a.abort('y');
console.log('any after', any.aborted, any.reason);

Perry evidence

Source review against origin/main:

  • crates/perry-runtime/src/url/abort.rs defines only three AbortSignal fields: aborted, reason, and _listeners.
  • js_abort_controller_abort_reason() stores the reason and fires listeners, but listener firing calls callbacks with no Event object and does not handle onabort or listener options such as { once: true }.
  • js_abort_signal_timeout(_ms) explicitly documents the current limitation: it returns a signal that is initially not aborted and does not start a real timer.
  • There is no runtime implementation for AbortSignal.abort(reason), AbortSignal.any(signals), or abortSignal.throwIfAborted().
  • crates/perry-codegen/src/expr/static_method.rs special-cases only AbortSignal.timeout(...).
  • crates/perry-codegen/src/lower_call/options/abort.rs handles only controller.abort(reason?) and controller.signal.addEventListener('abort', cb). Its static-timeout path is commented as skipped in the call-lowering path.
  • test-parity/node-suite/stream/abort/signal-timeout.ts already expects AbortSignal.timeout(ms) to abort after the delay, but the runtime helper currently returns a never-aborted signal.

Duplicate check

Searched existing issues and PRs for:

  • AbortSignal.any AbortSignal.abort throwIfAborted
  • AbortSignal timeout abort reason
  • AbortController AbortSignal
  • PR search for AbortSignal.any OR throwIfAborted OR AbortSignal.abort

No existing AbortSignal lifecycle/static-helper tracker matched. This is separate from #2575, which tracks global DOM Event / EventTarget constructors and dispatchEvent; this issue is specifically the AbortSignal lifecycle surface and static helper behavior.

Expected compatibility

  • Object.getOwnPropertyNames(AbortSignal) includes abort, timeout, and any.
  • Object.getOwnPropertyNames(AbortSignal.prototype) includes aborted, onabort, reason, and throwIfAborted.
  • AbortSignal.abort(reason) returns an already-aborted signal with that reason.
  • AbortSignal.timeout(delay) aborts after the delay with a Node-compatible timeout abort reason.
  • AbortSignal.any(signals) aborts when the first input signal aborts and adopts that signal's reason.
  • abortSignal.throwIfAborted() throws abortSignal.reason when aborted.
  • Abort listeners and onabort are invoked once per signal with a Node-compatible abort event shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions