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
2 changes: 2 additions & 0 deletions docs/test/dates-times.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ test("unlike in jest", () => {
});
```

Like in Jest, fake timers apply to the timers your code creates with `setTimeout` and `setInterval`. The timers Bun's built-in modules schedule for themselves, such as `socket.setTimeout()`, the callback of `server.listen()` or the `timeout` option of `child_process.exec()`, keep running in real time while fake timers are active.

## Reset the system time

To reset the system time, pass no arguments to `setSystemTime`:
Expand Down
18 changes: 18 additions & 0 deletions src/event_loop/EventLoopTimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// — to convert at every assignment and risked silent layout drift).
use Timespec as timespec;
pub use bun_core::Timespec;
use bun_core::TimespecMockMode;

// Re-export so higher tiers see the *same* type they pass to
// `bun_io::heap::Intrusive<EventLoopTimer, _>` (a zero-sized local stub
Expand Down Expand Up @@ -179,6 +180,11 @@ impl EventLoopTimer {
#[derive(Copy, Clone, Eq, PartialEq, strum::IntoStaticStr)]
pub enum Tag {
TimeoutObject,
/// A `TimeoutObject` scheduled by a built-in JS module through
/// `internal/timers` (socket idle timeouts, `child_process` kill timers,
/// ...): the same container as `TimeoutObject`, but a runtime-internal
/// timeout as far as `allow_fake_timers` is concerned.
Comment on lines +183 to +186

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

InternalTimeoutObject,
ImmediateObject,
StatWatcherScheduler,
UpgradedDuplex,
Expand Down Expand Up @@ -217,6 +223,18 @@ impl Tag {
Tag::TimeoutObject | Tag::AbortSignalTimeout | Tag::CronJob
)
}

/// The clock an owner with this tag arms with (the rule stated on
/// [`Self::allow_fake_timers`]), for owners that exist under more than one
/// tag: a real-heap timer armed from the mocked clock is due immediately,
/// and re-arms due immediately.
Comment on lines +227 to +230

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub fn clock(self) -> TimespecMockMode {
if self.allow_fake_timers() {
TimespecMockMode::AllowMockedTime
} else {
TimespecMockMode::ForceRealTime
}
}
}

/// Stamp out one `unsafe fn $method(*const EventLoopTimer) -> *mut Self` per
Expand Down
1 change: 1 addition & 0 deletions src/js/bun/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { SQLiteAdapter } = require("internal/sql/sqlite");
const { SQLHelper, parseOptions } = require("internal/sql/shared");

const { SQLError, PostgresError, SQLiteError, MySQLError } = require("internal/sql/errors");
const { setTimeout, clearTimeout } = require("internal/timers");

const defineProperties = Object.defineProperties;

Expand Down
3 changes: 3 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,9 @@ export const arrayBufferViewHasBuffer = $newCppFunction(

export const timerInternals = {
timerClockMs: $newRustFunction("runtime/timer/Timer.rs", "internal_bindings.timerClockMs", 0),
// The timers built-in modules schedule their own deadlines with; unlike the
// globals they are not touched by jest.useFakeTimers().
Comment on lines +560 to +561

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

internalTimers: require("internal/timers"),
};

// Raw datagram descriptor helpers for tests that need an unbound fd (which
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/cluster/child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const EventEmitter = require("node:events");
const Worker = require("internal/cluster/Worker");
const path = require("node:path");
const { kClusterOwner: owner_symbol, kInternalSendOptions } = require("internal/shared");
const { setInterval, clearInterval } = require("internal/timers");

const onInternalMessage = $newRustFunction("node_cluster_binding.rs", "onInternalMessageChild", 2);
const closeRawHandle = $newRustFunction("node_cluster_binding.rs", "clusterCloseHandle", 1);
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { isIPv4 } = require("internal/net/isIP");
const { setTimeout } = require("internal/timers");

const {
getHeader,
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/quic/quic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const {
isKeyObject,
} = require("node:util/types");
const { SocketAddress, BlockList } = require("node:net");
const { setTimeout, clearTimeout } = require("internal/timers");

// The native binding hands certificates over as DER bytes; expose them as
// X509Certificate objects like Node does.
Expand Down
2 changes: 1 addition & 1 deletion src/js/internal/readline/emitKeypressEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { SafeStringIterator, Symbol } = primordials;
const { charLengthAt, CSI, emitKeys } = require("internal/readline/utils");
const { kSawKeyPress } = require("internal/readline/interface");

const { clearTimeout, setTimeout } = require("node:timers");
const { clearTimeout, setTimeout } = require("internal/timers");
const { kEscape } = CSI;

const { StringDecoder } = require("node:string_decoder");
Expand Down
2 changes: 1 addition & 1 deletion src/js/internal/repl/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let debug = require("internal/repl/node-shims").debuglog("repl", fn => {
debug = fn;
});
const permission = require("internal/repl/node-shims");
const { clearTimeout, setTimeout } = require("node:timers");
const { clearTimeout, setTimeout } = require("internal/timers");
const { reverseString } = require("internal/readline/utils");

// The debounce is to guard against code pasted into the REPL.
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/sql/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
SQLQueryFlags,
symbols: { _strings, _values },
} = require("internal/sql/query");
const { setTimeout, clearTimeout } = require("internal/timers");

declare global {
interface NumberConstructor {
Expand Down
1 change: 1 addition & 0 deletions src/js/internal/streams/fast-utf8-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
validateString,
validateUint32,
} = require("internal/validators");
const { setTimeout, setInterval, clearInterval } = require("internal/timers");

const EventEmitter = require("node:events");
const path = require("node:path");
Expand Down
18 changes: 18 additions & 0 deletions src/js/internal/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ const NumberIsFinite = Number.isFinite;

const TIMEOUT_MAX = 2 ** 31 - 1;

// Timers for the runtime's own deadlines (socket idle timeouts, listen()
// callbacks, child_process kill timers, ...). The globals belong to user code:
// jest.useFakeTimers() freezes, counts, advances and clears every timer created
// through them, and user code may replace them outright. These create the same
// Timeout objects but never take part in fake timers, like the private timer
// references Node's lib/ uses. Built-in modules take all four from here
// (test/internal/source-lints/builtin-timer-globals.test.ts); the global
// clearTimeout would clear these too, the private one just stays out of reach
// of replaced globals.
Comment on lines +7 to +15

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

const setTimeout = $newCppFunction("node/NodeTimers.cpp", "functionSetTimeoutInternal", 1);
const setInterval = $newCppFunction("node/NodeTimers.cpp", "functionSetIntervalInternal", 1);
const clearTimeout = $newCppFunction("node/NodeTimers.cpp", "functionClearTimeout", 1);
const clearInterval = $newCppFunction("node/NodeTimers.cpp", "functionClearInterval", 1);

function getTimerDuration(msecs, name) {
validateNumber(msecs, name);
if (msecs < 0 || !NumberIsFinite(msecs)) {
Expand All @@ -29,4 +43,8 @@ export default {
// tests that inspect socket[kTimeout].
kTimeout: Symbol.for("::buntimeout::"),
getTimerDuration,
setTimeout,
setInterval,
clearTimeout,
clearInterval,
};
1 change: 1 addition & 0 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
validateOneOf,
} = require("internal/validators");
const { ConnResetException, hasObserver, startPerf, stopPerf, kInternalSendOptions } = require("internal/shared");
const { setTimeout, clearTimeout, setInterval, clearInterval } = require("internal/timers");
const kServerResponseStatistics = Symbol("ServerResponseStatistics");

const { isPrimary } = require("internal/cluster/isPrimary");
Expand Down
1 change: 1 addition & 0 deletions src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
validateObject,
validateOneOf,
} = require("internal/validators");
const { setTimeout, clearTimeout } = require("internal/timers");

var NetModule;

Expand Down
2 changes: 1 addition & 1 deletion src/js/node/http2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
const { isTypedArray } = require("node:util/types");
const { hideFromStack, hasObserver, enqueueNodeEntry, PerformanceNodeEntry } = require("internal/shared");
const { STATUS_CODES } = require("internal/http");
const { kTimeout, getTimerDuration } = require("internal/timers");
const { kTimeout, getTimerDuration, setTimeout, clearTimeout } = require("internal/timers");
const tls = require("node:tls");
const net = require("node:net");
const fs = require("node:fs");
Expand Down
2 changes: 1 addition & 1 deletion src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const {
import type { Socket, SocketHandler, SocketListener } from "bun";
import type { Server as NetServer, Socket as NetSocket, ServerOpts } from "node:net";
import type { TLSSocket } from "node:tls";
const { kTimeout, getTimerDuration } = require("internal/timers");
const { kTimeout, getTimerDuration, setTimeout, clearTimeout } = require("internal/timers");
const { validateFunction, validateNumber, validateAbortSignal, validatePort, validateBoolean, validateInt32, validateString } = require("internal/validators"); // prettier-ignore
const { isIPv4, isIPv6, isIP } = require("internal/net/isIP");
const { kArmHandshakeTimeout, kSecureConnectDone, kVerifyError } = require("internal/net/symbols");
Expand Down
5 changes: 2 additions & 3 deletions src/js/node/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ const kDefaultName = "<anonymous>";
const kRootName = "<root>";
const kDefaultFunction = () => {};
// The runner's own timers must keep working while `mock.timers` replaces the
// globals, so capture them at module load like Node's runner does.
const realSetTimeout = setTimeout;
const realClearTimeout = clearTimeout;
// globals or bun:test's fake timers are active, like Node's runner's do.
const { setTimeout: realSetTimeout, clearTimeout: realClearTimeout } = require("internal/timers");
const kDefaultOptions = kEmptyObject;
// Matches Node's internal/timers TIMEOUT_MAX.
const kTimeoutMax = 2 ** 31 - 1;
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 36 additions & 58 deletions src/jsc/bindings/node/NodeTimers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@

#include "ErrorCode.h"
#include "headers.h"
#include <wtf/text/MakeString.h>

namespace Bun {

using namespace JSC;

JSC_DEFINE_HOST_FUNCTION(functionSetTimeout,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
using TimerScheduler = JSC::EncodedJSValue (*)(JSC::JSGlobalObject*, JSC::EncodedJSValue callback, JSC::EncodedJSValue arguments, JSC::EncodedJSValue countdown);

// setTimeout(callback, delay, ...args) / setInterval(callback, delay, ...args).
// The extra arguments are packed the way Bun__JSTimeout__call (NodeTimerObject.cpp)
// unpacks them: undefined for none, the value itself for one, a JSCellButterfly
// for several.
Comment on lines +13 to +16

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

static JSC::EncodedJSValue scheduleTimer(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, ASCIILiteral name, TimerScheduler schedule)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSValue job = callFrame->argument(0);
JSC::JSValue num = callFrame->argument(1);
JSC::JSValue arguments = jsUndefined();
size_t argumentCount = callFrame->argumentCount();
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
switch (argumentCount) {

switch (callFrame->argumentCount()) {
case 0: {
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "setTimeout requires 1 argument (a function)"_s);
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, makeString(name, " requires 1 argument (a function)"_s));
return {};
}
case 1:
Expand All @@ -44,7 +50,7 @@ JSC_DEFINE_HOST_FUNCTION(functionSetTimeout,
}

if (!job.isObject() || !job.getObject()->isCallable()) [[unlikely]] {
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "setTimeout expects a function"_s);
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, makeString(name, " expects a function"_s));
return {};
}

Expand All @@ -60,64 +66,36 @@ JSC_DEFINE_HOST_FUNCTION(functionSetTimeout,
}
#endif

return Bun__Timer__setTimeout(globalObject, JSC::JSValue::encode(job), JSC::JSValue::encode(arguments), JSValue::encode(num));
RELEASE_AND_RETURN(scope, schedule(globalObject, JSC::JSValue::encode(job), JSC::JSValue::encode(arguments), JSC::JSValue::encode(num)));
}

JSC_DEFINE_HOST_FUNCTION(functionSetInterval,
JSC_DEFINE_HOST_FUNCTION(functionSetTimeout,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
JSC::JSValue job = callFrame->argument(0);
JSC::JSValue num = callFrame->argument(1);
JSC::JSValue arguments = jsUndefined();
size_t argumentCount = callFrame->argumentCount();
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());

switch (argumentCount) {
case 0: {
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "setInterval requires 1 argument (a function)"_s);
return {};
}
case 1:
case 2: {
break;
}
case 3: {
arguments = callFrame->argument(2);
break;
}

default: {
ArgList argumentsList = ArgList(callFrame, 2);
auto* args = JSC::JSCellButterfly::tryCreateFromArgList(vm, argumentsList);

if (!args) [[unlikely]] {
JSC::throwOutOfMemoryError(globalObject, scope);
return {};
}

arguments = JSValue(args);
}
}
return scheduleTimer(globalObject, callFrame, "setTimeout"_s, Bun__Timer__setTimeout);
}

if (!job.isObject() || !job.getObject()->isCallable()) [[unlikely]] {
Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "setInterval expects a function"_s);
return {};
}
JSC_DEFINE_HOST_FUNCTION(functionSetInterval,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
return scheduleTimer(globalObject, callFrame, "setInterval"_s, Bun__Timer__setInterval);
}

#ifdef BUN_DEBUG
/** View the file name of the JS file that called this function
* from a debugger */
SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm);
auto fileNameUTF8 = sourceOrigin.string().utf8();
const char* fileName = fileNameUTF8.data();
static const char* lastFileName = nullptr;
if (lastFileName != fileName) {
lastFileName = fileName;
}
#endif
// The setTimeout/setInterval that built-in JS modules schedule their own
// deadlines with (src/js/internal/timers.ts). Same arguments and same Timeout
// object as the globals, but the timer is never handed to bun:test's fake
// timers, so socket timeouts, listen() callbacks and the like keep working
// while a test has jest.useFakeTimers() active.
Comment on lines +84 to +88

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

JSC_DEFINE_HOST_FUNCTION(functionSetTimeoutInternal,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
return scheduleTimer(globalObject, callFrame, "setTimeout"_s, Bun__Timer__setTimeoutInternal);
}

return Bun__Timer__setInterval(globalObject, JSC::JSValue::encode(job), JSC::JSValue::encode(arguments), JSValue::encode(num));
JSC_DEFINE_HOST_FUNCTION(functionSetIntervalInternal,
(JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
return scheduleTimer(globalObject, callFrame, "setInterval"_s, Bun__Timer__setIntervalInternal);
}

// https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/node/NodeTimers.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ JSC_DECLARE_HOST_FUNCTION(functionClearTimeout);
JSC_DECLARE_HOST_FUNCTION(functionClearInterval);
JSC_DECLARE_HOST_FUNCTION(functionClearImmediate);

// Reached only through $newCppFunction in src/js/internal/timers.ts.
JSC_DECLARE_HOST_FUNCTION(functionSetTimeoutInternal);
JSC_DECLARE_HOST_FUNCTION(functionSetIntervalInternal);

} // namespace Bun
2 changes: 1 addition & 1 deletion src/runtime/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ pub(crate) unsafe fn __bun_fire_timer(t: *mut EventLoopTimer, now: *const ElTime
}
match tag {
// ── JS-exposed timers (TimerObjectInternals::fire) ───────────────
EventLoopTimerTag::TimeoutObject => {
EventLoopTimerTag::TimeoutObject | EventLoopTimerTag::InternalTimeoutObject => {
let container = owner!(TimeoutObject, event_loop_timer);
// SAFETY: container derived from a live `TimeoutObject`; do NOT
// form `&mut *container` — `internals.fire` may `deref()` and free.
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/timer/ImmediateObject.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use bun_jsc::virtual_machine::VirtualMachine;
use bun_jsc::{JSGlobalObject, JSValue};

use super::{Kind, TimerObjectInternals};
use super::{EventLoopTimerTag, Kind, TimerObjectInternals};

// `jsc.Codegen.JSImmediate` — the C++ JSCell wrapper stays generated; this
// struct is the `m_ctx` payload. Struct + `RefCounted`/`Default` impls + the
// struct is the `m_ctx` payload. Struct + `RefCounted` impl + the
// forwarder host-fns (`to_primitive`/`do_ref`/`do_unref`/`has_ref`/
// `get_destroyed`/`dispose`/`constructor`/`finalize`/`ref_`/`deref`/`deinit`/
// `init_with`) — see `impl_timer_object!` in `super` (timer/mod.rs).
super::impl_timer_object!(ImmediateObject, ImmediateObject, "Immediate");
super::impl_timer_object!(ImmediateObject, "Immediate");

impl ImmediateObject {
pub(crate) fn init(
Expand All @@ -17,7 +17,15 @@ impl ImmediateObject {
callback: JSValue,
arguments: JSValue,
) -> JSValue {
Self::init_with(global, id, Kind::SetImmediate, 0, callback, arguments)
Self::init_with(
global,
EventLoopTimerTag::ImmediateObject,
id,
Kind::SetImmediate,
0,
callback,
arguments,
)
}

/// Thin forwarder to
Expand Down
Loading