mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 03:48:56 +00:00
feat: implement vi.advanceTimersToNextTick()
This commit is contained in:
25
packages/bun-types/test.d.ts
vendored
25
packages/bun-types/test.d.ts
vendored
@@ -191,6 +191,31 @@ declare module "bun:test" {
|
||||
useFakeTimers: typeof jest.useFakeTimers;
|
||||
useRealTimers: typeof jest.useRealTimers;
|
||||
runAllTicks: typeof jest.runAllTicks;
|
||||
/**
|
||||
* Advances all timers to the next timer (earliest scheduled timer).
|
||||
* Useful for stepping through timers one at a time.
|
||||
*
|
||||
* @returns this (the vi object) for chaining
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { vi, test, expect } from "bun:test";
|
||||
*
|
||||
* test("vi.advanceTimersToNextTimer", () => {
|
||||
* vi.useFakeTimers();
|
||||
*
|
||||
* let i = 0;
|
||||
* setInterval(() => console.log(++i), 50);
|
||||
*
|
||||
* vi.advanceTimersToNextTimer() // log: 1
|
||||
* .advanceTimersToNextTimer() // log: 2
|
||||
* .advanceTimersToNextTimer(); // log: 3
|
||||
*
|
||||
* vi.useRealTimers();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
advanceTimersToNextTimer(): typeof vi;
|
||||
};
|
||||
|
||||
interface FunctionLike {
|
||||
|
||||
@@ -578,6 +578,35 @@ pub const All = struct {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
/// Internal function that advances to the next timer in the vi_timers heap.
|
||||
/// Returns true if a timer was fired, false if no timers remain.
|
||||
fn advanceToNextViTimer(timer: *All, vm: *VirtualMachine) bool {
|
||||
// Get the next timer from the heap
|
||||
timer.lock.lock();
|
||||
const event_timer = timer.vi_timers.deleteMin();
|
||||
timer.lock.unlock();
|
||||
|
||||
if (event_timer) |et| {
|
||||
// Set the virtual time to this timer's scheduled time
|
||||
// This ensures nested timers scheduled during callbacks get the correct base time
|
||||
timer.vi_current_time = et.next;
|
||||
|
||||
// Fire the timer at its scheduled time
|
||||
const arm_result = et.fire(&et.next, vm);
|
||||
|
||||
// If the timer wants to rearm (like setInterval), reinsert it
|
||||
if (arm_result == .rearm) {
|
||||
timer.lock.lock();
|
||||
timer.vi_timers.insert(et);
|
||||
timer.lock.unlock();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn runAllTimers(
|
||||
globalThis: *JSGlobalObject,
|
||||
_: *jsc.CallFrame,
|
||||
@@ -588,27 +617,7 @@ pub const All = struct {
|
||||
|
||||
// Keep firing timers until there are no more
|
||||
// We need to keep checking the heap because firing timers can schedule new timers
|
||||
while (true) {
|
||||
timer.lock.lock();
|
||||
const event_timer = timer.vi_timers.deleteMin();
|
||||
timer.lock.unlock();
|
||||
|
||||
if (event_timer == null) break;
|
||||
|
||||
// Set the virtual time to this timer's scheduled time
|
||||
// This ensures nested timers scheduled during callbacks get the correct base time
|
||||
timer.vi_current_time = event_timer.?.next;
|
||||
|
||||
// Fire the timer at its scheduled time
|
||||
const arm_result = event_timer.?.fire(&event_timer.?.next, vm);
|
||||
|
||||
// If the timer wants to rearm (like setInterval), reinsert it
|
||||
if (arm_result == .rearm) {
|
||||
timer.lock.lock();
|
||||
timer.vi_timers.insert(event_timer.?);
|
||||
timer.lock.unlock();
|
||||
}
|
||||
}
|
||||
while (advanceToNextViTimer(timer, vm)) {}
|
||||
|
||||
// Clear the virtual time when we're done
|
||||
timer.vi_current_time = null;
|
||||
@@ -616,6 +625,28 @@ pub const All = struct {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
pub fn advanceTimersToNextTimer(
|
||||
globalThis: *JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) JSError!JSValue {
|
||||
jsc.markBinding(@src());
|
||||
const vm = globalThis.bunVM();
|
||||
const timer = &vm.timer;
|
||||
|
||||
// Advance to the next timer (if any)
|
||||
_ = advanceToNextViTimer(timer, vm);
|
||||
|
||||
// Keep the virtual time set for subsequent calls (don't clear it)
|
||||
// Return the 'this' value (the vi object) for chaining
|
||||
return callframe.this();
|
||||
}
|
||||
|
||||
export fn Bun__Timer__initViTime(vm: *VirtualMachine) callconv(.C) void {
|
||||
const timer = &vm.timer;
|
||||
// Initialize virtual time to zero so timers are scheduled relative to time 0
|
||||
timer.vi_current_time = timespec{ .sec = 0, .nsec = 0 };
|
||||
}
|
||||
|
||||
export fn Bun__Timer__clearViTimers(vm: *VirtualMachine) callconv(.C) void {
|
||||
const timer = &vm.timer;
|
||||
timer.lock.lock();
|
||||
@@ -636,7 +667,9 @@ pub const All = struct {
|
||||
@export(&jsc.host_fn.wrap2(clearTimeout), .{ .name = "Bun__Timer__clearTimeout" });
|
||||
@export(&jsc.host_fn.wrap2(clearInterval), .{ .name = "Bun__Timer__clearInterval" });
|
||||
@export(&jsc.host_fn.wrap2(runAllTimers), .{ .name = "Bun__Timer__runAllTimers" });
|
||||
@export(&jsc.host_fn.wrap2(advanceTimersToNextTimer), .{ .name = "Bun__Timer__advanceTimersToNextTimer" });
|
||||
@export(&getNextID, .{ .name = "Bun__Timer__getNextID" });
|
||||
_ = &Bun__Timer__initViTime;
|
||||
_ = &Bun__Timer__clearViTimers;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1423,6 +1423,8 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionWithImplementation, (JSC::JSGlobalObject
|
||||
using namespace Bun;
|
||||
using namespace JSC;
|
||||
|
||||
extern "C" void Bun__Timer__initViTime(void*);
|
||||
|
||||
// Enables fake timers and sets up Date mocking
|
||||
BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseFakeTimers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
|
||||
{
|
||||
@@ -1433,6 +1435,14 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsUseFakeTimers, (JSC::JSGlobalObject * globalO
|
||||
// Initialize with current time if not already set
|
||||
globalObject->overridenDateNow = globalObject->jsDateNow();
|
||||
}
|
||||
|
||||
// Initialize vi_current_time so timers are scheduled relative to time 0
|
||||
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
auto* vm = bunGlobal->bunVM();
|
||||
if (vm) {
|
||||
Bun__Timer__initViTime(vm);
|
||||
}
|
||||
|
||||
return JSValue::encode(callframe->thisValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ pub const Jest = struct {
|
||||
vi.put(globalObject, ZigString.static("isFakeTimers"), jsc.host_fn.NewFunction(globalObject, ZigString.static("isFakeTimers"), 0, JSMock__jsIsFakeTimers, false));
|
||||
vi.put(globalObject, ZigString.static("runAllTimers"), jsc.host_fn.NewFunction(globalObject, ZigString.static("runAllTimers"), 0, Bun__Timer__runAllTimers, false));
|
||||
vi.put(globalObject, ZigString.static("runAllTicks"), jsc.host_fn.NewFunction(globalObject, ZigString.static("runAllTicks"), 0, JSMock__jsRunAllTicks, false));
|
||||
vi.put(globalObject, ZigString.static("advanceTimersToNextTimer"), jsc.host_fn.NewFunction(globalObject, ZigString.static("advanceTimersToNextTimer"), 0, Bun__Timer__advanceTimersToNextTimer, false));
|
||||
module.put(globalObject, ZigString.static("vi"), vi);
|
||||
}
|
||||
|
||||
@@ -278,6 +279,7 @@ pub const Jest = struct {
|
||||
extern fn JSMock__jsUseFakeTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
|
||||
extern fn JSMock__jsUseRealTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
|
||||
extern fn Bun__Timer__runAllTimers(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
|
||||
extern fn Bun__Timer__advanceTimersToNextTimer(*JSGlobalObject, *CallFrame) callconv(jsc.conv) JSValue;
|
||||
|
||||
pub fn call(
|
||||
globalObject: *JSGlobalObject,
|
||||
|
||||
@@ -3,22 +3,35 @@ import { vi, test, beforeAll, afterAll, expect } from "vitest";
|
||||
beforeAll(() => vi.useFakeTimers());
|
||||
afterAll(() => vi.useRealTimers());
|
||||
|
||||
test.each(Array.from({ length: 2 }).map((_, i) => i))("runAllTimers runs in order of time", i => {
|
||||
const order: string[] = [];
|
||||
const orderNum: number[] = [];
|
||||
|
||||
let base = 0;
|
||||
const time = (d: number, l: string, cb: () => void) => {
|
||||
const start = base;
|
||||
class TimeHelper {
|
||||
base: number = 0;
|
||||
order: string[] = [];
|
||||
orderNum: number[] = [];
|
||||
time(d: number, l: string, cb: () => void) {
|
||||
const start = this.base;
|
||||
setTimeout(() => {
|
||||
orderNum.push(start + d);
|
||||
order.push(`${start + d}${l ? ` (${l})` : ""}`);
|
||||
if (base != 0) throw new Error("base is not 0");
|
||||
base = start + d;
|
||||
this.addOrder(start + d, l);
|
||||
if (this.base != 0) throw new Error("base is not 0");
|
||||
this.base = start + d;
|
||||
cb();
|
||||
base = 0;
|
||||
this.base = 0;
|
||||
}, d);
|
||||
};
|
||||
}
|
||||
addOrder(d: number, l: string) {
|
||||
this.orderNum.push(d);
|
||||
this.order.push(`${d}${l ? ` (${l})` : ""}`);
|
||||
}
|
||||
expectOrder() {
|
||||
expect(this.orderNum).toEqual(this.orderNum.toSorted((a, b) => a - b));
|
||||
const order = this.order;
|
||||
this.order = [];
|
||||
return expect(order);
|
||||
}
|
||||
}
|
||||
|
||||
test.each(["runAllTimers", "advanceTimersToNextTimer"])("%s runs in order of time", mode => {
|
||||
const tester = new TimeHelper();
|
||||
const time = tester.time.bind(tester);
|
||||
|
||||
time(1000, "", () => {
|
||||
time(500, "", () => {});
|
||||
@@ -41,18 +54,30 @@ test.each(Array.from({ length: 2 }).map((_, i) => i))("runAllTimers runs in orde
|
||||
const interval = setInterval(() => {
|
||||
if (intervalCount > 3) clearInterval(interval);
|
||||
intervalCount += 1;
|
||||
orderNum.push(intervalCount * 499);
|
||||
order.push(`${intervalCount * 499} (interval)`);
|
||||
tester.addOrder(intervalCount * 499, "interval");
|
||||
setTimeout(() => {
|
||||
orderNum.push(intervalCount * 499 + 25);
|
||||
order.push(`${intervalCount * 499 + 25} (interval + 25)`);
|
||||
tester.addOrder(intervalCount * 499 + 25, "interval + 25");
|
||||
}, 25);
|
||||
}, 499);
|
||||
|
||||
vi.runAllTimers();
|
||||
if (mode === "runAllTimers") {
|
||||
vi.runAllTimers();
|
||||
} else if (mode === "advanceTimersToNextTimer") {
|
||||
let orderLen = 0;
|
||||
while (true) {
|
||||
vi.advanceTimersToNextTimer();
|
||||
if (tester.order.length > orderLen) {
|
||||
expect(tester.order.length).toBe(orderLen + 1);
|
||||
orderLen = tester.order.length;
|
||||
} else if (tester.order.length === orderLen) {
|
||||
break;
|
||||
} else {
|
||||
expect.fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(orderNum).toEqual(orderNum.toSorted((a, b) => a - b));
|
||||
expect(order).toMatchInlineSnapshot(`
|
||||
tester.expectOrder().toMatchInlineSnapshot(`
|
||||
[
|
||||
"0 (zero 1)",
|
||||
"0 (zero 2)",
|
||||
@@ -88,8 +113,6 @@ test("runAllTimers supports interval", () => {
|
||||
if (ticks >= 10) clearInterval(interval);
|
||||
}, 25);
|
||||
|
||||
vi.runAllTimers();
|
||||
|
||||
expect(ticks).toBe(10);
|
||||
});
|
||||
|
||||
|
||||
205
test/js/bun/test/vi-advance-timers-to-next-timer.test.ts
Normal file
205
test/js/bun/test/vi-advance-timers-to-next-timer.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { expect, test, vi } from "bun:test";
|
||||
|
||||
test("vi.advanceTimersToNextTimer() advances to next timer", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let i = 0;
|
||||
setInterval(() => i++, 50);
|
||||
|
||||
expect(i).toBe(0);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(i).toBe(1);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(i).toBe(2);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(i).toBe(3);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() is chainable", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let i = 0;
|
||||
setInterval(() => i++, 50);
|
||||
|
||||
expect(i).toBe(0);
|
||||
|
||||
vi.advanceTimersToNextTimer().advanceTimersToNextTimer().advanceTimersToNextTimer();
|
||||
|
||||
expect(i).toBe(3);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() handles multiple timers with different delays", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const order: number[] = [];
|
||||
|
||||
setTimeout(() => order.push(1), 100);
|
||||
setTimeout(() => order.push(2), 50);
|
||||
setTimeout(() => order.push(3), 150);
|
||||
|
||||
expect(order).toEqual([]);
|
||||
|
||||
// Should fire in order of scheduled time, not order they were created
|
||||
vi.advanceTimersToNextTimer(); // fires timer at 50ms
|
||||
expect(order).toEqual([2]);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // fires timer at 100ms
|
||||
expect(order).toEqual([2, 1]);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // fires timer at 150ms
|
||||
expect(order).toEqual([2, 1, 3]);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() handles nested timers", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const order: number[] = [];
|
||||
|
||||
setTimeout(() => {
|
||||
order.push(1);
|
||||
setTimeout(() => order.push(3), 50);
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => order.push(2), 150);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // fires timer at 100ms, schedules new timer at current_time + 50ms
|
||||
expect(order).toEqual([1]);
|
||||
|
||||
// When two timers are at the same time (150ms), they fire in the order they were scheduled (FIFO)
|
||||
// Timer 2 was scheduled first (at time 0), so it fires before timer 3 (scheduled during callback at time 100)
|
||||
vi.advanceTimersToNextTimer(); // fires timer at 150ms (timer 2, scheduled first)
|
||||
expect(order).toEqual([1, 2]);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // fires nested timer at 150ms (timer 3, scheduled during callback)
|
||||
expect(order).toEqual([1, 2, 3]);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() does nothing when no timers are pending", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let called = false;
|
||||
setTimeout(() => {
|
||||
called = true;
|
||||
}, 100);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(called).toBe(true);
|
||||
|
||||
// No more timers, this should be safe to call
|
||||
vi.advanceTimersToNextTimer();
|
||||
vi.advanceTimersToNextTimer();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() works with setInterval", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let count = 0;
|
||||
const intervalId = setInterval(() => {
|
||||
count++;
|
||||
if (count >= 3) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
expect(count).toBe(0);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(count).toBe(1);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(count).toBe(2);
|
||||
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(count).toBe(3);
|
||||
|
||||
// Interval was cleared, no more timers
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(count).toBe(3);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() handles mix of setTimeout and setInterval", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const events: string[] = [];
|
||||
|
||||
setTimeout(() => events.push("timeout-1"), 50);
|
||||
const intervalId = setInterval(() => {
|
||||
events.push("interval");
|
||||
if (events.filter(e => e === "interval").length >= 2) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 100);
|
||||
setTimeout(() => events.push("timeout-2"), 200);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // 50ms: timeout-1
|
||||
expect(events).toEqual(["timeout-1"]);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // 100ms: interval (1st)
|
||||
expect(events).toEqual(["timeout-1", "interval"]);
|
||||
|
||||
vi.advanceTimersToNextTimer(); // 200ms: interval (2nd) and timeout-2 compete
|
||||
// The interval at 200ms should fire first (it was scheduled first), then gets cleared
|
||||
expect(events.length).toBe(3);
|
||||
expect(events).toContain("interval");
|
||||
|
||||
vi.advanceTimersToNextTimer(); // 200ms: timeout-2
|
||||
expect(events).toContain("timeout-2");
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() executes timer callbacks with automatic microtask processing", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const order: string[] = [];
|
||||
|
||||
setTimeout(() => {
|
||||
order.push("timer");
|
||||
process.nextTick(() => order.push("tick"));
|
||||
}, 100);
|
||||
|
||||
// When a timer callback runs, microtasks (including nextTick) are processed automatically
|
||||
// This is standard JavaScript execution behavior
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(order).toEqual(["timer", "tick"]);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("vi.advanceTimersToNextTimer() returns vi object for chaining with other vi methods", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let timerCalled = false;
|
||||
let tickCalled = false;
|
||||
|
||||
setTimeout(() => {
|
||||
timerCalled = true;
|
||||
process.nextTick(() => {
|
||||
tickCalled = true;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Microtasks run automatically after timer callback, so runAllTicks() is not needed here
|
||||
// but chaining still works
|
||||
vi.advanceTimersToNextTimer();
|
||||
|
||||
expect(timerCalled).toBe(true);
|
||||
expect(tickCalled).toBe(true);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
Reference in New Issue
Block a user