Skip to content

Expose Node's Immediate._onImmediate on setImmediate() return value - #31802

Open
robobun wants to merge 5 commits into
mainfrom
farm/8626f818/immediate-on-immediate
Open

Expose Node's Immediate._onImmediate on setImmediate() return value#31802
robobun wants to merge 5 commits into
mainfrom
farm/8626f818/immediate-on-immediate

Conversation

@robobun

@robobun robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #31801.

Repro

const fn = () => {};
const immediate = setImmediate(fn);
console.log('immediate._onImmediate =', immediate._onImmediate);
console.log(typeof immediate._onImmediate);
clearImmediate(immediate);

Node:

immediate._onImmediate = [Function: fn]
function

Bun (before):

immediate._onImmediate = undefined
undefined

Cause

setImmediate() returns an Immediate object. Node exposes the scheduled callback on its _onImmediate property — the sibling Timeout object already does the equivalent with _onTimeout. Bun's Immediate class had no _onImmediate accessor, so the property read back undefined.

_onImmediate isn't a spec requirement, but Node-based test suites and internal inspection logic read it.

Fix

The callback is already stored in the cached callback slot at construction (TimerObjectInternals::init writes it via JSImmediate::callback_set_cached unconditionally). This mirrors Timeout._onTimeout for Immediate:

  • src/runtime/node/node.classes.ts — add an _onImmediate getter/setter to the Immediate proto (same shape as _onTimeout).
  • src/runtime/timer/ImmediateObject.rs — add a js module with codegen_cached_accessors!("Immediate"; arguments, callback,) and get_on_immediate/set_on_immediate reading/writing the cached callback slot — verbatim the TimeoutObject.rs pattern.

_onImmediate is exposed as a prototype accessor (getter + setter), identical to how _onTimeout is exposed, and returns the exact callback the user passed.

Verification

New tests in test/js/node/timers/node-timers.test.ts (_onImmediate describe):

  • exposes the scheduled callback (typeof is function, identity === fn)
  • is a prototype accessor, like Timeout._onTimeout
  • is writable
  • mirrors Timeout._onTimeout

3 of 4 fail on USE_SYSTEM_BUN=1 bun test (returning undefined) and all 4 pass with bun bd test. Full node-timers.test.ts is green (24/24).

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds an _onImmediate prototype accessor to Immediate instances: Rust exposes cached getter/setter functions, the TS class maps them to _onImmediate, and tests verify getter/setter behavior and parity with Timeout._onTimeout.

Changes

Immediate._onImmediate Property Implementation

Layer / File(s) Summary
Callback accessor implementation
src/runtime/timer/ImmediateObject.rs
Rust-level cached property accessors (get_on_immediate / set_on_immediate) are added via codegen bindings for the Immediate class, enabling read/write of the stored callback.
Class property binding
src/runtime/node/node.classes.ts
TypeScript class definition maps the Rust callback accessors to the _onImmediate property on Immediate's prototype, exposing get/set accessors on instances.
Accessor property tests
test/js/node/timers/node-timers.test.ts
Test suite validates _onImmediate returns the scheduled callback, is implemented as a prototype accessor, is writable via the prototype setter without creating an own property, supports reassignment before firing, and mirrors Timeout._onTimeout semantics.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: exposing Node's Immediate._onImmediate property on setImmediate() return values, which matches the primary objective of the PR.
Description check ✅ Passed The description exceeds template requirements with detailed explanation of the problem, cause, fix, and verification; it covers what the PR does and how it was verified.
Linked Issues check ✅ Passed The PR fully implements the requirement from issue #31801: exposing the _onImmediate property on setImmediate() return values with getter/setter accessors matching Node behavior.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to exposing _onImmediate: prototype accessor in node.classes.ts, cached callback accessors in ImmediateObject.rs, and related tests in node-timers.test.ts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions github-actions Bot added the claude label Jun 4, 2026
@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Comment thread src/runtime/timer/ImmediateObject.rs
Comment thread src/runtime/timer/ImmediateObject.rs Outdated
Comment thread test/js/node/timers/node-timers.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — verbatim mirror of the existing _onTimeout accessor pattern, and all prior feedback has been addressed.

Extended reasoning...

Overview

This PR exposes Immediate._onImmediate as a prototype getter/setter, matching Node.js. It touches three files: node.classes.ts adds a 5-line proto entry identical in shape to the existing _onTimeout entry; ImmediateObject.rs adds a js module with codegen_cached_accessors! and a get_on_immediate/set_on_immediate pair that are line-for-line copies of TimeoutObject.rs's get_on_timeout/set_on_timeout; and node-timers.test.ts gains 5 focused tests.

Security risks

None. The accessor reads/writes a cached WriteBarrier slot that already existed and was already populated at construction — this just exposes it to JS, exactly as Timeout._onTimeout already does. No new memory management, no new untrusted-input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Low. The implementation is a mechanical copy of an established sibling pattern (TimeoutObject.rsImmediateObject.rs, _onTimeout_onImmediate) with no novel logic. The cached callback slot was already declared in the Immediate class's values array and written by TimerObjectInternals::init; this PR only adds the JS-visible accessor over it.

Other factors

All three of my earlier review threads are resolved: (1) the _onImmediate = null concern was refuted with a verified Node v24.3.0 repro showing Node also throws — so Bun's behavior is correct parity; (2) the AsyncContextFrame wrapper observation is pre-existing on _onTimeout and out of scope; (3) the test-validity nit was fixed in 4ae8b3e by asserting no own-property is created after the write. A 5th test ("fires the reassigned callback") was added in fa878ce to prove the setter actually feeds dispatch. The CI failures (musl LTO link error, bunx.test.ts on asan) are unrelated to timers. No CODEOWNERS cover these paths.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI note for reviewers: the red lanes on the prior run are unrelated to this diff (a Node timer accessor — no platform, install, network, or Bake code). Verified from the job logs:

  • debian 13 x64-asan - test-bun: all 336 test files passed (0 failures, no core dumps). The job exited non-zero on CI infrastructure — ci-remap server did not start: timeout with the Docker test-service containers (mysql/redis/minio) stuck in "Creating".
  • windows 2019 x64 - test-bun and …-baseline: the only failing test is test/cli/install/bunx.test.ts › should handle package that requires node 24 (fetches a real npm package; flaky on Windows CI) plus a test/bake/dev/production.test.ts SSR case. Neither touches timers.

No failure references node-timers / _onImmediate. The debian-13-x64-asan-test-bun lane is the ASAN/gate-equivalent and the new _onImmediate tests passed there. Pushed one ci: retrigger to re-roll the flaky lanes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@test/js/node/timers/node-timers.test.ts`:
- Around line 196-203: Wrap the test body that creates the immediate in a
try/finally and call clearImmediate(immediate) in the finally block to match
other tests' cleanup; specifically, keep the Promise.withResolvers usage and the
reassignment of immediate._onImmediate (the resolve/reject logic) but ensure you
always call clearImmediate(immediate) in a finally to defensively clean up the
immediate created by setImmediate.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 500318c2-2602-4673-b7b3-b255644d28a5

📥 Commits

Reviewing files that changed from the base of the PR and between 32f8070 and df10bde.

📒 Files selected for processing (1)
  • test/js/node/timers/node-timers.test.ts

Comment thread test/js/node/timers/node-timers.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — verbatim mirror of the existing _onTimeout accessor pattern; prior feedback addressed, and the remaining clearImmediate nulling note is a pre-existing gap shared with _onTimeout that's fine as a follow-up.

Extended reasoning...

Overview

This PR adds an _onImmediate getter/setter to the Immediate prototype for Node.js compat (issue #31801). It touches three files: a 5-line addition to node.classes.ts (proto entry identical in shape to the adjacent _onTimeout), ~30 lines in ImmediateObject.rs (a codegen_cached_accessors! module + getter/setter that read/write the existing cached callback slot — copied verbatim from TimeoutObject.rs:65-75), and 5 new tests in node-timers.test.ts.

Security risks

None. This exposes a read/write accessor for a callback slot that was already stored and already invoked by the dispatch path; no new data flows, no auth/crypto/permissions surface, no untrusted input parsing.

Level of scrutiny

Low. The implementation is a mechanical copy of the established _onTimeout pattern in the same module — same macro, same slot, same signatures. The cached callback slot was already populated at init and already consumed by run_immediate_task; this PR only adds a JS-visible accessor over it. No event-loop, scheduling, or lifetime logic changes.

Other factors

All three prior inline concerns are resolved: the null-_onImmediate behavior was verified against Node v24.3.0 (Node also throws, so Bun's behavior is correct as-is); the AsyncContextFrame wrapper note is acknowledged as a pre-existing _onTimeout limitation and out of scope; and the test-validity nit was fixed in 4ae8b3e (the writable test now asserts no own-property is created, and a new "fires the reassigned callback" test directly proves the setter writes the dispatch slot). The one remaining inline nit (clearImmediate doesn't null the slot) is explicitly low-impact and applies equally to the pre-existing clearTimeout/_onTimeout path — reasonable to fix both together in a follow-up. CI failures (bunx.test.ts, x64-musl link) are unrelated to timers.

Comment thread src/runtime/timer/ImmediateObject.rs
@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI status — diff is green; the only red is unrelated flake, needs a maintainer to merge.

I've now seen this across two runs (#60302, #60303). Every build lane (rust/cpp/bun, all platforms) passes. The sole failing test is test/cli/install/bunx.test.ts › should handle package that requires node 24, which fetches a real npm package and asserts its exit code — it failed with Received: 3 after 4 retries on both the debian-13-x64-asan and Windows test-bun lanes. It has nothing to do with this change (a Node timer accessor); no failure references node-timers, _onImmediate, or ImmediateObject. The ASAN lane ran the new _onImmediate tests and they passed.

I've already used my one re-roll, so I'm not pushing another retrigger (that would just be noise). This needs a maintainer to merge past the flaky install test, or a re-run of that lane.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI status (final): the diff is green; both red lanes are confirmed external and already tracked.

  1. All test-bun lanes fail on exactly one test, bunx.test.ts › should handle package that requires node 24. Root cause confirmed locally: @angular/cli@latest now requires Node >= 24.15.0 and Bun reports v24.3.0, so the CLI's own version gate exits 3. This reproduces identically on the released bun with no changes (USE_SYSTEM_BUN=1 bun x --bun @angular/cli@latest --help → exit 3), so it is unrelated to this PR and breaks every branch. Tracked in CI: bunx.test.ts fails on all platforms — @angular/cli@latest now requires Node >= 24.15.0, bun reports 24.3.0 #31797, test fix in test: pin @angular/cli version in bunx node-version test #31820.
  2. windows x64-baseline verify-baseline fails on a 0-byte Intel SDE download. Same failure on main (build 60790); maintainers are baking SDE into the Windows image (main build 60873).

node-timers.test.ts (this PR's tests) passes on every lane it ran. Once #31820 lands, a rerun should be green. Needs a maintainer to merge (or rerun after the test fix lands).

robobun and others added 5 commits June 5, 2026 20:41
setImmediate() returns an Immediate object; Node exposes the scheduled
callback on its _onImmediate property, mirroring Timeout._onTimeout.
Bun's Immediate had no such accessor, so immediate._onImmediate was
undefined. Node-based test suites and inspection logic rely on it.

The callback is already stored in the cached "callback" slot at
construction (TimerObjectInternals::init), so this adds an _onImmediate
getter/setter to the Immediate class that reads/writes that slot,
exactly mirroring Timeout._onTimeout.
Adds coverage proving the writable _onImmediate accessor is authoritative
over dispatch: reassigning it before the immediate fires swaps which
callback runs, matching Node. Fails on baseline (plain-property
assignment is ignored by dispatch, so the original fires).
Strengthens the writable test so it exercises set_on_immediate rather
than passing trivially: without the accessor, assignment creates an own
expando data property, so asserting no own property exists after the
write makes the test fail on a runtime lacking the accessor.
@robobun
robobun force-pushed the farm/8626f818/immediate-on-immediate branch from df10bde to e580ac7 Compare June 5, 2026 20:45

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — verbatim mirror of the existing _onTimeout accessor pattern, and all prior review threads are resolved.

Extended reasoning...

Overview

This PR adds an _onImmediate getter/setter to the Immediate prototype for Node compatibility, fixing #31801. Three files touched: a 5-line addition to node.classes.ts (the proto entry), ~25 lines in ImmediateObject.rs (a js module with codegen_cached_accessors! plus get_on_immediate/set_on_immediate), and 5 new tests in node-timers.test.ts. The Rust implementation is byte-for-byte the same as TimeoutObject.rs's get_on_timeout/set_on_timeout — it reads/writes the cached callback slot that TimerObjectInternals::init already populates.

Security risks

None. This exposes a read/write accessor for a callback slot the user already controls (they passed it to setImmediate). The setter lets JS overwrite it, but that's identical to the long-shipping _onTimeout setter and matches Node's behavior. No auth, crypto, filesystem, or network surface.

Level of scrutiny

Low. The change is mechanical pattern replication: the _onTimeout accessor on Timeout already exists with the exact same shape ({getter, setter, this: true} in node.classes.ts, cached-slot read/write in Rust), and this PR copies it onto Immediate. There's no novel logic or design decision — the callback is already stored in the slot at construction; this just exposes it. No CODEOWNERS cover these paths.

Other factors

I previously raised four points on this PR; all are resolved. The author refuted my null-assignment concern with empirical Node verification (Node also throws on im._onImmediate = null, so Bun's behavior matches), fixed the test-validity nit by adding a getOwnPropertyDescriptor assertion (and a "fires the reassigned callback" dispatch test), and correctly scoped the two pre-existing _onTimeout parity gaps (AsyncContextFrame wrapper, post-clear nulling) to a follow-up. CodeRabbit's one nitpick was correctly declined. The bug-hunting system found nothing this run. CI is green for the change itself — the only red is a CI-wide bunx.test.ts npm-registry flake unrelated to timers, and the ASAN lane passed the new tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun’s setImmediate() return value does not expose Node’s Immediate._onImmediate internal field

1 participant