mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(timers): add _idleStart property to Timeout object (#26021)
## Summary
- Add `_idleStart` property (getter/setter) to the Timeout object
returned by `setTimeout()` and `setInterval()`
- The property returns a monotonic timestamp (in milliseconds)
representing when the timer was created
- This mimics Node.js's behavior where `_idleStart` is the libuv
timestamp at timer creation time
## Test plan
- [x] Verified test fails with `USE_SYSTEM_BUN=1 bun test
test/regression/issue/25639.test.ts`
- [x] Verified test passes with `bun bd test
test/regression/issue/25639.test.ts`
- [x] Manual verification:
```bash
# Bun with fix - _idleStart exists
./build/debug/bun-debug -e "const t = setTimeout(() => {}, 0);
console.log('_idleStart' in t, typeof t._idleStart); clearTimeout(t)"
# Output: true number
# Node.js reference - same behavior
node -e "const t = setTimeout(() => {}, 0); console.log('_idleStart' in
t, typeof t._idleStart); clearTimeout(t)"
# Output: true number
```
Closes #25639
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,14 @@ pub fn set_repeat(_: *Self, thisValue: JSValue, globalThis: *JSGlobalObject, val
|
||||
Self.js.repeatSetCached(thisValue, globalThis, value);
|
||||
}
|
||||
|
||||
pub fn get_idleStart(_: *Self, thisValue: JSValue, _: *JSGlobalObject) JSValue {
|
||||
return Self.js.idleStartGetCached(thisValue).?;
|
||||
}
|
||||
|
||||
pub fn set_idleStart(_: *Self, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void {
|
||||
Self.js.idleStartSetCached(thisValue, globalThis, value);
|
||||
}
|
||||
|
||||
pub fn dispose(self: *Self, globalThis: *JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
|
||||
self.internals.cancel(globalThis.bunVM());
|
||||
return .js_undefined;
|
||||
|
||||
@@ -242,7 +242,7 @@ fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer
|
||||
this.strong_this.set(global, timer);
|
||||
this.flags.kind = .setInterval;
|
||||
this.interval = new_interval;
|
||||
this.reschedule(timer, vm);
|
||||
this.reschedule(timer, vm, global);
|
||||
}
|
||||
|
||||
pub fn run(this: *TimerObjectInternals, globalThis: *jsc.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, vm: *jsc.VirtualMachine) bool {
|
||||
@@ -293,8 +293,8 @@ pub fn init(
|
||||
TimeoutObject.js.idleTimeoutSetCached(timer, global, .jsNumber(interval));
|
||||
TimeoutObject.js.repeatSetCached(timer, global, if (kind == .setInterval) .jsNumber(interval) else .null);
|
||||
|
||||
// this increments the refcount
|
||||
this.reschedule(timer, vm);
|
||||
// this increments the refcount and sets _idleStart
|
||||
this.reschedule(timer, vm, global);
|
||||
}
|
||||
|
||||
this.strong_this.set(global, timer);
|
||||
@@ -328,7 +328,7 @@ pub fn doRefresh(this: *TimerObjectInternals, globalObject: *jsc.JSGlobalObject,
|
||||
}
|
||||
|
||||
this.strong_this.set(globalObject, this_value);
|
||||
this.reschedule(this_value, VirtualMachine.get());
|
||||
this.reschedule(this_value, VirtualMachine.get(), globalObject);
|
||||
|
||||
return this_value;
|
||||
}
|
||||
@@ -371,7 +371,7 @@ fn shouldRescheduleTimer(this: *TimerObjectInternals, repeat: JSValue, idle_time
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine) void {
|
||||
pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine, globalThis: *JSGlobalObject) void {
|
||||
if (this.flags.kind == .setImmediate) return;
|
||||
|
||||
const idle_timeout = TimeoutObject.js.idleTimeoutGetCached(timer).?;
|
||||
@@ -380,7 +380,8 @@ pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachi
|
||||
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612
|
||||
if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return;
|
||||
|
||||
const now = timespec.msFromNow(.allow_mocked_time, this.interval);
|
||||
const now = timespec.now(.allow_mocked_time);
|
||||
const scheduled_time = now.addMs(this.interval);
|
||||
const was_active = this.eventLoopTimer().state == .ACTIVE;
|
||||
if (was_active) {
|
||||
vm.timer.remove(this.eventLoopTimer());
|
||||
@@ -388,9 +389,13 @@ pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachi
|
||||
this.ref();
|
||||
}
|
||||
|
||||
vm.timer.update(this.eventLoopTimer(), &now);
|
||||
vm.timer.update(this.eventLoopTimer(), &scheduled_time);
|
||||
this.flags.has_cleared_timer = false;
|
||||
|
||||
// Set _idleStart to the current monotonic timestamp in milliseconds
|
||||
// This mimics Node.js's behavior where _idleStart is the libuv timestamp when the timer was scheduled
|
||||
TimeoutObject.js.idleStartSetCached(timer, globalThis, .jsNumber(now.msUnsigned()));
|
||||
|
||||
if (this.flags.has_js_ref) {
|
||||
this.setEnableKeepingEventLoopAlive(vm, true);
|
||||
}
|
||||
|
||||
@@ -184,13 +184,18 @@ export default [
|
||||
setter: "set_repeat",
|
||||
this: true,
|
||||
},
|
||||
_idleStart: {
|
||||
getter: "get_idleStart",
|
||||
setter: "set_idleStart",
|
||||
this: true,
|
||||
},
|
||||
["@@dispose"]: {
|
||||
fn: "dispose",
|
||||
length: 0,
|
||||
invalidThisBehavior: InvalidThisBehavior.NoOp,
|
||||
},
|
||||
},
|
||||
values: ["arguments", "callback", "idleTimeout", "repeat"],
|
||||
values: ["arguments", "callback", "idleTimeout", "repeat", "idleStart"],
|
||||
}),
|
||||
define({
|
||||
name: "Immediate",
|
||||
|
||||
64
test/regression/issue/25639.test.ts
Normal file
64
test/regression/issue/25639.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
// GitHub Issue #25639: setTimeout Timeout object missing _idleStart property
|
||||
// Next.js 16 uses _idleStart to coordinate timers for Cache Components
|
||||
|
||||
test("setTimeout returns Timeout object with _idleStart property", () => {
|
||||
const timer = setTimeout(() => {}, 100);
|
||||
|
||||
try {
|
||||
// Verify _idleStart exists and is a number
|
||||
expect("_idleStart" in timer).toBe(true);
|
||||
expect(typeof timer._idleStart).toBe("number");
|
||||
|
||||
// _idleStart should be a positive timestamp
|
||||
expect(timer._idleStart).toBeGreaterThan(0);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
test("setInterval returns Timeout object with _idleStart property", () => {
|
||||
const timer = setInterval(() => {}, 100);
|
||||
|
||||
try {
|
||||
// Verify _idleStart exists and is a number
|
||||
expect("_idleStart" in timer).toBe(true);
|
||||
expect(typeof timer._idleStart).toBe("number");
|
||||
|
||||
// _idleStart should be a positive timestamp
|
||||
expect(timer._idleStart).toBeGreaterThan(0);
|
||||
} finally {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
|
||||
test("_idleStart is writable (Next.js modifies it to coordinate timers)", () => {
|
||||
const timer = setTimeout(() => {}, 100);
|
||||
|
||||
try {
|
||||
const originalIdleStart = timer._idleStart;
|
||||
expect(typeof originalIdleStart).toBe("number");
|
||||
|
||||
// Next.js sets _idleStart to coordinate timers
|
||||
const newIdleStart = originalIdleStart - 100;
|
||||
timer._idleStart = newIdleStart;
|
||||
expect(timer._idleStart).toBe(newIdleStart);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
test("timers created at different times have different _idleStart values", async () => {
|
||||
const timer1 = setTimeout(() => {}, 100);
|
||||
// Wait a bit to ensure different timestamp
|
||||
await Bun.sleep(10);
|
||||
const timer2 = setTimeout(() => {}, 100);
|
||||
|
||||
try {
|
||||
expect(timer2._idleStart).toBeGreaterThanOrEqual(timer1._idleStart);
|
||||
} finally {
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user