Skip to content

fix(runtime): AbortSignal parity — accept in events helpers + dynamic method dispatch#6046

Merged
proggeramlug merged 2 commits into
mainfrom
fix/events-helpers-accept-abort-signal
Jul 6, 2026
Merged

fix(runtime): AbortSignal parity — accept in events helpers + dynamic method dispatch#6046
proggeramlug merged 2 commits into
mainfrom
fix/events-helpers-accept-abort-signal

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

Node treats AbortSignal as an EventTarget. Perry represents it as its own native object (url/abort.rs) that two dispatch layers didn't recognize, which breaks real minified applications in two ways:

1. Module-level node:events helpers reject signals.

const { setMaxListeners } = require("node:events");
setMaxListeners(50, controller.signal);
// ERR_INVALID_ARG_TYPE: The "eventTargets" argument must be an instance
// of EventEmitter or EventTarget. Received an instance of Object

This is the standard way SDKs raise the MaxListenersExceededWarning threshold on a shared signal; the throw rejects the caller's whole request path.

2. Signal methods vanish on dynamically-typed receivers.

c.signal.addEventListener("abort", f);        // works (static lowering)
const s = c.signal;                            // untyped local — minified SDK shape
s.addEventListener("abort", f);                // threw: addEventListener is not a function
typeof s.addEventListener                      // was "undefined"

Same class as the #5964 URLSearchParams dynamic-dispatch wall.

Fix

  • events helpers (perry-stdlib/events/module_helpers.rs + perry-ext-events/lib.rs, kept in sync): recognize signals ahead of event_helper_target in setMaxListeners / getMaxListeners / listenerCount / getEventListeners. Set is a faithful no-op (the warning threshold is the call's only Node-observable effect and Perry never emits that warning for signals — documented inline), get reports the EventTarget default (10), and the listener queries report the signal's registered "abort" listeners via two new runtime helpers (js_abort_signal_listener_count, js_abort_signal_listeners_copy — a copy, so callers can't mutate the internal list).
  • dynamic dispatch (get_field_by_name_tail.rs + native_call_method.rs): property GET returns a bound-method closure (abort_signal_method_bind, signal captured as its NaN-boxed bits so the GC scan keeps/relocates it), and the fused dynamic method CALL pre-dispatches addEventListener / removeEventListener / throwIfAborted to the natives (the URLSearchParams methods unreachable through dynamically-typed access (append is not a function) #5961 precedent block). options is accepted and ignored — a signal only ever fires "abort" once, so { once: true } is behaviorally implied.

Tests

crates/perry/tests/issue_events_helpers_abort_signal.rs (2 e2e compile+run tests):

  • helpers surface: set=ok max=10 count0=0 count1=1 list1=1 other=0 and a plain object still throws.
  • dynamic receivers: typeof s.addEventListener === "function", 2-arg and 3-arg registration, removeEventListener works, listeners actually fire on abort (postAbortFired=11), throwIfAborted no-ops while pending and throws after abort.

Both pass; cargo build clean on the touched crates.

Summary by CodeRabbit

  • New Features

    • Added first-class AbortSignal support to node:events helper APIs (getEventListeners, listenerCount, getMaxListeners, setMaxListeners) for "abort" listeners.
    • Dynamic AbortSignal method calls now correctly dispatch addEventListener, removeEventListener, and throwIfAborted.
  • Bug Fixes

    • setMaxListeners(signal) no longer throws for AbortSignal and is treated as a safe no-op for signals.
    • getMaxListeners(signal) returns the default cap (10), and non-"abort" events return empty/zero results.
  • Tests

    • Added coverage for helper behavior and method dispatch with both typed and dynamically-typed signals.

… method dispatch

Node treats `AbortSignal` as an `EventTarget`; Perry represents it as its
own native object (url/abort.rs) that two dispatch layers didn't know:

1. The module-level `node:events` helpers. `setMaxListeners(n,
   controller.signal)` — how SDKs raise the MaxListenersExceededWarning
   threshold on a shared signal — threw `ERR_INVALID_ARG_TYPE: The
   "eventTargets" argument must be an instance of EventEmitter or
   EventTarget`, rejecting the caller's whole request path. The four
   helpers (`setMaxListeners`, `getMaxListeners`, `listenerCount`,
   `getEventListeners`) now recognize signals in both perry-stdlib and
   perry-ext-events: set is a faithful no-op (the warning threshold is the
   call's only Node-observable effect and Perry never emits that warning
   for signals), get reports the EventTarget default, and the listener
   queries report the signal's registered "abort" listeners through two
   new runtime helpers (`js_abort_signal_listener_count`,
   `js_abort_signal_listeners_copy`).

2. Dynamic receivers. The statically-typed form
   (`c.signal.addEventListener(...)`) lowers to the native call, but the
   same method through an untyped local — the shape minified SDK code
   takes — resolved to `undefined` and threw `addEventListener is not a
   function` (the #5964 URLSearchParams dynamic-dispatch class). Both
   dynamic paths are covered: property GET returns a bound-method closure
   (`abort_signal_method_bind` in get_field_by_name_tail), and the fused
   dynamic method CALL pre-dispatches to the natives in
   js_native_call_method (the #5961 precedent) for `addEventListener` /
   `removeEventListener` / `throwIfAborted`. `options` is accepted and
   ignored — a signal only ever fires "abort" once, so `{ once: true }`
   is behaviorally implied.

Two e2e tests cover the helpers surface and the dynamic-receiver forms
(including listeners actually firing on abort and plain objects still
being rejected).
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 550d3222-3f13-4123-95cb-1044bc853183

📥 Commits

Reviewing files that changed from the base of the PR and between a0fe6d9 and af9af22.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/object/native_call_method.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-runtime/src/object/native_call_method.rs

📝 Walkthrough

Walkthrough

This PR adds AbortSignal support to Node-compatible node:events helpers and runtime dispatch. New FFI functions expose AbortSignal listener counts and listener copies. Dynamic property and method dispatch now binds addEventListener, removeEventListener, and throwIfAborted on AbortSignal receivers. Tests verify the new behavior.

Changes

AbortSignal Events Integration

Layer / File(s) Summary
Core AbortSignal listener/method FFI
crates/perry-runtime/src/url/abort.rs
Adds bound-method thunks, abort_signal_method_bind, abort_listeners_array, js_abort_signal_listener_count, and js_abort_signal_listeners_copy.
Runtime property and method dispatch
crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs, crates/perry-runtime/src/object/native_call_method.rs
Routes AbortSignal property reads and native calls to the new bound methods.
events helpers AbortSignal special-casing
crates/perry-ext-events/src/lib.rs, crates/perry-stdlib/src/events/module_helpers.rs
Adds AbortSignal-aware handling for getEventListeners, listenerCount, getMaxListeners, and setMaxListeners.
Integration tests for AbortSignal events helpers
crates/perry/tests/issue_events_helpers_abort_signal.rs
Adds TypeScript-based tests for helper behavior and dynamic AbortSignal method dispatch.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JSCode
  participant Runtime
  participant EventsHelpers
  participant abort.rs as abort.rs

  JSCode->>Runtime: signal.addEventListener(...)
  Runtime->>abort.rs: abort_signal_method_bind / listener thunk
  abort.rs-->>Runtime: bound method or dispatch result

  JSCode->>EventsHelpers: listenerCount(signal, "abort")
  EventsHelpers->>abort.rs: js_abort_signal_listener_count(signal)
  abort.rs-->>EventsHelpers: count
  EventsHelpers-->>JSCode: count

  JSCode->>EventsHelpers: getEventListeners(signal, "abort")
  EventsHelpers->>abort.rs: js_abort_signal_listeners_copy(signal)
  abort.rs-->>EventsHelpers: copied array
  EventsHelpers-->>JSCode: listeners array
Loading

Possibly related PRs

  • PerryTS/perry#6004: Both PRs add a receiver-type fast path in crates/perry-runtime/src/object/native_call_method.rs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: AbortSignal parity for events helpers and dynamic method dispatch.
Description check ✅ Passed The description covers the problem, fix, and tests clearly, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/events-helpers-accept-abort-signal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-ext-events/src/lib.rs (1)

1855-1864: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

AbortSignal special-casing logic is duplicated verbatim in perry-stdlib/src/events/module_helpers.rs.

Same detection-and-dispatch logic (resolve ptr, check "abort" event name, return listeners/count, no-op on setMaxListeners) appears in both crates. This mirrors an existing pre-PR pattern for other abort helpers in this file, so it's consistent with the codebase, but it means any future change to signal semantics (e.g., honoring per-signal max-listener overrides) needs to be kept in sync in two places.

Consider extracting the shared "resolve + dispatch" logic into a single helper in perry-runtime that both crates can call, to avoid drift.

Also applies to: 1895-1902, 1925-1930, 1956-1967

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-ext-events/src/lib.rs` around lines 1855 - 1864, The AbortSignal
handling is duplicated between this module and the matching helpers in
perry-stdlib, so update the shared “resolve ptr + dispatch on abort” logic to
live in a single helper in perry-runtime and have the existing abort-related
entry points call it. Keep the behavior centralized for the functions around
js_abort_signal_resolve_ptr, js_abort_signal_listeners_copy, and the abort-only
branches so future signal-semantic changes only need one implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/native_call_method.rs`:
- Around line 741-775: Guard the AbortSignal fast path in native_call_method
before reading recv_ptr.class_id, since pointer-tagged handle-band values can
enter this branch and dereferencing them may hit unmapped memory. Update the
addEventListener/removeEventListener/throwIfAborted path to use the same
handle-band vs object-pointer validation already used elsewhere in
native_call_method.rs, so only real object pointers reach the
ABORT_SIGNAL_CLASS_ID check and other values fall through to the normal
TypeError path.

---

Nitpick comments:
In `@crates/perry-ext-events/src/lib.rs`:
- Around line 1855-1864: The AbortSignal handling is duplicated between this
module and the matching helpers in perry-stdlib, so update the shared “resolve
ptr + dispatch on abort” logic to live in a single helper in perry-runtime and
have the existing abort-related entry points call it. Keep the behavior
centralized for the functions around js_abort_signal_resolve_ptr,
js_abort_signal_listeners_copy, and the abort-only branches so future
signal-semantic changes only need one implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 612e32d4-0234-424f-907d-f11064464aee

📥 Commits

Reviewing files that changed from the base of the PR and between afe0e2d and a0fe6d9.

📒 Files selected for processing (6)
  • crates/perry-ext-events/src/lib.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-runtime/src/url/abort.rs
  • crates/perry-stdlib/src/events/module_helpers.rs
  • crates/perry/tests/issue_events_helpers_abort_signal.rs

Comment thread crates/perry-runtime/src/object/native_call_method.rs
… receivers

A native handle (Headers/fetch/timer/...) is nanbox-pointer-tagged but its
"pointer" is a small integer id, not a heap ObjectHeader. Guard the
class_id read in the fused dynamic-call AbortSignal arm with is_small_handle
so a handle receiver never gets dereferenced as an object.
@proggeramlug proggeramlug merged commit 2ec9a80 into main Jul 6, 2026
25 checks passed
@proggeramlug proggeramlug deleted the fix/events-helpers-accept-abort-signal branch July 6, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant