Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/runtime/node/node.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ export default [
_destroyed: {
getter: "getDestroyed",
},
_onImmediate: {
getter: "get_onImmediate",
setter: "set_onImmediate",
this: true,
},
["@@dispose"]: {
fn: "dispose",
length: 0,
Expand Down
32 changes: 32 additions & 0 deletions src/runtime/timer/ImmediateObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ use bun_jsc::{JSGlobalObject, JSValue};

use super::{Kind, TimerObjectInternals};

/// Only the cached-property accessors — `callbackGetCached` / `callbackSetCached`
/// etc. per `values` entry in the `Immediate` `.classes.ts` define — are declared
/// here; the rest of the C++ JSCell wrapper is emitted by the `#[JsClass]` derive.
pub mod js {
bun_jsc::codegen_cached_accessors!(
"Immediate";
arguments,
callback,
);
}

// `jsc.Codegen.JSImmediate` — the C++ JSCell wrapper stays generated; this
// struct is the `m_ctx` payload. Struct + `RefCounted`/`Default` impls + the
// forwarder host-fns (`to_primitive`/`do_ref`/`do_unref`/`has_ref`/
Expand All @@ -20,6 +31,27 @@ impl ImmediateObject {
Self::init_with(global, id, Kind::SetImmediate, 0, callback, arguments)
}

// Cached-property getter/setter — codegen passes `this_value` (the JS
// wrapper) so the cached `WriteBarrier` slot on the C++ side can be read/written.
// Mirrors `Timeout::get_on_timeout`/`set_on_timeout` (see `TimeoutObject.rs`).

pub fn get_on_immediate(
_this: &Self,
this_value: JSValue,
_global: &JSGlobalObject,
) -> JSValue {
js::callback_get_cached(this_value).unwrap()
}
Comment thread
robobun marked this conversation as resolved.

pub fn set_on_immediate(
_this: &Self,
this_value: JSValue,
global: &JSGlobalObject,
value: JSValue,
) {
js::callback_set_cached(this_value, global, value);
}
Comment thread
robobun marked this conversation as resolved.

/// Thin forwarder to
/// `internals.run_immediate_task`. Reached from `bun_jsc::event_loop`
/// via `__bun_run_immediate_task` (definer in [`crate::dispatch`]).
Expand Down
61 changes: 61 additions & 0 deletions test/js/node/timers/node-timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,67 @@ describe("_destroyed", () => {
});
});

// https://github.com/oven-sh/bun/issues/31801
describe("_onImmediate", () => {
it("exposes the scheduled callback", () => {
const fn = () => {};
const immediate = setImmediate(fn) as any;
try {
expect(typeof immediate._onImmediate).toBe("function");
expect(immediate._onImmediate).toBe(fn);
} finally {
clearImmediate(immediate);
}
});

it("is a prototype accessor, like Timeout._onTimeout", () => {
const immediate = setImmediate(() => {}) as any;
try {
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(immediate), "_onImmediate");
expect(typeof descriptor?.get).toBe("function");
expect(typeof descriptor?.set).toBe("function");
} finally {
clearImmediate(immediate);
}
});

it("is writable through the prototype setter", () => {
const replacement = () => {};
const immediate = setImmediate(() => {}) as any;
try {
immediate._onImmediate = replacement;
expect(immediate._onImmediate).toBe(replacement);
// The write must route through the prototype accessor, not create an own
// expando data property (which is what a runtime lacking the accessor does).
expect(Object.getOwnPropertyDescriptor(immediate, "_onImmediate")).toBeUndefined();
} finally {
clearImmediate(immediate);
}
});

it("fires the reassigned callback, not the original", async () => {
// Matches Node: the callback invoked at dispatch is whatever _onImmediate
// currently holds, so reassigning it before it fires swaps what runs.
const { promise, resolve, reject } = Promise.withResolvers<string>();
const immediate = setImmediate(() => reject(new Error("original callback fired"))) as any;
immediate._onImmediate = () => resolve("replacement");
expect(await promise).toBe("replacement");
});
Comment thread
robobun marked this conversation as resolved.

it("mirrors Timeout's _onTimeout", () => {
const fn = () => {};
const timeout = setTimeout(fn, 0) as any;
const immediate = setImmediate(fn) as any;
try {
expect(immediate._onImmediate).toBe(timeout._onTimeout);
expect(immediate._onImmediate).toBe(fn);
} finally {
clearTimeout(timeout);
clearImmediate(immediate);
}
});
});

describe("clear", () => {
it("can clear the other kind of timer", async () => {
const timeout1 = setTimeout(() => {
Expand Down
Loading