mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 12:29:07 +00:00
## Summary - Adds birthtime (file creation time) support on Linux using the `statx` syscall - Stores birthtime in architecture-specific unused fields of the kernel Stat struct (x86_64 and aarch64) - Falls back to traditional `stat` on kernels < 4.11 that don't support `statx` - Includes comprehensive tests validating birthtime behavior Fixes #6585 ## Implementation Details **src/sys.zig:** - Added `StatxField` enum for field selection - Implemented `statxImpl()`, `fstatx()`, `statx()`, and `lstatx()` functions - Stores birthtime in unused padding fields (architecture-specific for x86_64 and aarch64) - Graceful fallback to traditional stat if statx is not supported **src/bun.js/node/node_fs.zig:** - Updated `stat()`, `fstat()`, and `lstat()` to use statx functions on Linux **src/bun.js/node/Stat.zig:** - Added `getBirthtime()` helper to extract birthtime from architecture-specific storage **test/js/node/fs/fs-birthtime-linux.test.ts:** - Tests non-zero birthtime values - Verifies birthtime immutability across file modifications - Validates consistency across stat/lstat/fstat - Tests BigInt stats with nanosecond precision - Verifies birthtime ordering relative to other timestamps ## Test Plan - [x] Run `bun bd test test/js/node/fs/fs-birthtime-linux.test.ts` - all 5 tests pass - [x] Compare behavior with Node.js - identical behavior - [x] Compare with system Bun - system Bun returns epoch, new implementation returns real birthtime 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
546 lines
20 KiB
Zig
546 lines
20 KiB
Zig
const log = bun.Output.scoped(.StatWatcher, .visible);
|
|
|
|
fn statToJSStats(globalThis: *jsc.JSGlobalObject, stats: *const bun.sys.PosixStat, bigint: bool) bun.JSError!jsc.JSValue {
|
|
if (bigint) {
|
|
return StatsBig.init(stats).toJS(globalThis);
|
|
} else {
|
|
return StatsSmall.init(stats).toJS(globalThis);
|
|
}
|
|
}
|
|
|
|
/// This is a singleton struct that contains the timer used to schedule re-stat calls.
|
|
pub const StatWatcherScheduler = struct {
|
|
current_interval: std.atomic.Value(i32) = .{ .raw = 0 },
|
|
task: jsc.WorkPoolTask = .{ .callback = &workPoolCallback },
|
|
main_thread: std.Thread.Id,
|
|
vm: *bun.jsc.VirtualMachine,
|
|
watchers: WatcherQueue = WatcherQueue{},
|
|
|
|
event_loop_timer: EventLoopTimer = .{
|
|
.next = .epoch,
|
|
.tag = .StatWatcherScheduler,
|
|
},
|
|
|
|
ref_count: RefCount,
|
|
|
|
const RefCount = bun.ptr.ThreadSafeRefCount(StatWatcherScheduler, "ref_count", deinit, .{ .debug_name = "StatWatcherScheduler" });
|
|
pub const ref = RefCount.ref;
|
|
pub const deref = RefCount.deref;
|
|
|
|
const WatcherQueue = UnboundedQueue(StatWatcher, .next);
|
|
|
|
pub fn init(vm: *bun.jsc.VirtualMachine) bun.ptr.RefPtr(StatWatcherScheduler) {
|
|
return .new(.{
|
|
.ref_count = .init(),
|
|
.main_thread = std.Thread.getCurrentId(),
|
|
.vm = vm,
|
|
});
|
|
}
|
|
|
|
fn deinit(this: *StatWatcherScheduler) void {
|
|
bun.assertf(this.watchers.isEmpty(), "destroying StatWatcherScheduler while it still has watchers", .{});
|
|
bun.destroy(this);
|
|
}
|
|
|
|
pub fn append(this: *StatWatcherScheduler, watcher: *StatWatcher) void {
|
|
log("append new watcher {s}", .{watcher.path});
|
|
bun.assert(watcher.closed == false);
|
|
bun.assert(watcher.next == null);
|
|
|
|
watcher.ref();
|
|
this.watchers.push(watcher);
|
|
log("push watcher {x}", .{@intFromPtr(watcher)});
|
|
const current = this.getInterval();
|
|
if (current == 0 or current > watcher.interval) {
|
|
// we are not running or the new watcher has a smaller interval
|
|
this.setInterval(watcher.interval);
|
|
}
|
|
}
|
|
|
|
fn getInterval(this: *StatWatcherScheduler) i32 {
|
|
return this.current_interval.load(.monotonic);
|
|
}
|
|
|
|
/// Update the current interval and set the timer (this function is thread safe)
|
|
fn setInterval(this: *StatWatcherScheduler, interval: i32) void {
|
|
this.ref();
|
|
this.current_interval.store(interval, .monotonic);
|
|
|
|
if (this.main_thread == std.Thread.getCurrentId()) {
|
|
// we are in the main thread we can set the timer
|
|
this.setTimer(interval);
|
|
return;
|
|
}
|
|
// we are not in the main thread we need to schedule a task to set the timer
|
|
this.scheduleTimerUpdate();
|
|
}
|
|
|
|
/// Set the timer (this function is not thread safe, should be called only from the main thread)
|
|
fn setTimer(this: *StatWatcherScheduler, interval: i32) void {
|
|
|
|
// if the interval is 0 means that we stop the timer
|
|
if (interval == 0) {
|
|
// if the timer is active we need to remove it
|
|
if (this.event_loop_timer.state == .ACTIVE) {
|
|
this.vm.timer.remove(&this.event_loop_timer);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// reschedule the timer
|
|
this.vm.timer.update(&this.event_loop_timer, &bun.timespec.msFromNow(interval));
|
|
}
|
|
|
|
/// Schedule a task to set the timer in the main thread
|
|
fn scheduleTimerUpdate(this: *StatWatcherScheduler) void {
|
|
const Holder = struct {
|
|
scheduler: *StatWatcherScheduler,
|
|
task: jsc.AnyTask,
|
|
|
|
pub fn updateTimer(self: *@This()) void {
|
|
defer bun.default_allocator.destroy(self);
|
|
self.scheduler.setTimer(self.scheduler.getInterval());
|
|
}
|
|
};
|
|
const holder = bun.handleOom(bun.default_allocator.create(Holder));
|
|
holder.* = .{
|
|
.scheduler = this,
|
|
.task = jsc.AnyTask.New(Holder, Holder.updateTimer).init(holder),
|
|
};
|
|
this.vm.enqueueTaskConcurrent(jsc.ConcurrentTask.create(jsc.Task.init(&holder.task)));
|
|
}
|
|
|
|
pub fn timerCallback(this: *StatWatcherScheduler) EventLoopTimer.Arm {
|
|
const has_been_cleared = this.event_loop_timer.state == .CANCELLED or this.vm.scriptExecutionStatus() != .running;
|
|
|
|
this.event_loop_timer.state = .FIRED;
|
|
this.event_loop_timer.heap = .{};
|
|
|
|
if (has_been_cleared) {
|
|
return .disarm;
|
|
}
|
|
|
|
jsc.WorkPool.schedule(&this.task);
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
pub fn workPoolCallback(task: *jsc.WorkPoolTask) void {
|
|
var this: *StatWatcherScheduler = @alignCast(@fieldParentPtr("task", task));
|
|
// ref'd when the timer was scheduled
|
|
defer this.deref();
|
|
// Instant.now will not fail on our target platforms.
|
|
const now = std.time.Instant.now() catch unreachable;
|
|
|
|
var batch = this.watchers.popBatch();
|
|
log("pop batch of {d} watchers", .{batch.count});
|
|
var iter = batch.iterator();
|
|
var min_interval: i32 = std.math.maxInt(i32);
|
|
var closest_next_check: u64 = @intCast(min_interval);
|
|
var contain_watchers = false;
|
|
while (iter.next()) |watcher| {
|
|
if (watcher.closed) {
|
|
watcher.deref();
|
|
continue;
|
|
}
|
|
contain_watchers = true;
|
|
|
|
const time_since = now.since(watcher.last_check);
|
|
const interval = @as(u64, @intCast(watcher.interval)) * 1_000_000;
|
|
|
|
if (time_since >= interval -| 500) {
|
|
watcher.last_check = now;
|
|
watcher.restat();
|
|
} else {
|
|
closest_next_check = @min(interval - @as(u64, time_since), closest_next_check);
|
|
}
|
|
min_interval = @min(min_interval, watcher.interval);
|
|
this.watchers.push(watcher);
|
|
log("reinsert watcher {x}", .{@intFromPtr(watcher)});
|
|
}
|
|
|
|
if (contain_watchers) {
|
|
// choose the smallest interval or the closest time to the next check
|
|
this.setInterval(@min(min_interval, @as(i32, @intCast(closest_next_check))));
|
|
} else {
|
|
// we do not have watchers, we can stop the timer
|
|
this.setInterval(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: make this a top-level struct
|
|
pub const StatWatcher = struct {
|
|
pub const Scheduler = StatWatcherScheduler;
|
|
|
|
next: ?*StatWatcher = null,
|
|
|
|
ctx: *VirtualMachine,
|
|
|
|
ref_count: RefCount,
|
|
|
|
/// Closed is set to true to tell the scheduler to remove from list and deref.
|
|
closed: bool,
|
|
path: [:0]u8,
|
|
persistent: bool,
|
|
bigint: bool,
|
|
interval: i32,
|
|
last_check: std.time.Instant,
|
|
|
|
globalThis: *jsc.JSGlobalObject,
|
|
js_this: jsc.JSValue,
|
|
|
|
poll_ref: bun.Async.KeepAlive = .{},
|
|
|
|
last_stat: bun.sys.PosixStat,
|
|
last_jsvalue: jsc.Strong.Optional,
|
|
|
|
scheduler: bun.ptr.RefPtr(StatWatcherScheduler),
|
|
|
|
const RefCount = bun.ptr.ThreadSafeRefCount(StatWatcher, "ref_count", deinit, .{ .debug_name = "StatWatcher" });
|
|
pub const ref = RefCount.ref;
|
|
pub const deref = RefCount.deref;
|
|
|
|
pub const js = jsc.Codegen.JSStatWatcher;
|
|
pub const toJS = js.toJS;
|
|
pub const fromJS = js.fromJS;
|
|
pub const fromJSDirect = js.fromJSDirect;
|
|
|
|
pub fn eventLoop(this: StatWatcher) *EventLoop {
|
|
return this.ctx.eventLoop();
|
|
}
|
|
|
|
pub fn enqueueTaskConcurrent(this: StatWatcher, task: *jsc.ConcurrentTask) void {
|
|
this.eventLoop().enqueueTaskConcurrent(task);
|
|
}
|
|
|
|
pub fn deinit(this: *StatWatcher) void {
|
|
log("deinit {x}", .{@intFromPtr(this)});
|
|
|
|
if (this.persistent) {
|
|
this.persistent = false;
|
|
this.poll_ref.unref(this.ctx);
|
|
}
|
|
this.closed = true;
|
|
this.last_jsvalue.deinit();
|
|
|
|
bun.default_allocator.free(this.path);
|
|
bun.default_allocator.destroy(this);
|
|
}
|
|
|
|
pub const Arguments = struct {
|
|
path: PathLike,
|
|
listener: jsc.JSValue,
|
|
|
|
persistent: bool,
|
|
bigint: bool,
|
|
interval: i32,
|
|
|
|
global_this: *jsc.JSGlobalObject,
|
|
|
|
pub fn fromJS(global: *jsc.JSGlobalObject, arguments: *ArgumentsSlice) bun.JSError!Arguments {
|
|
const path = try PathLike.fromJSWithAllocator(global, arguments, bun.default_allocator) orelse {
|
|
return global.throwInvalidArguments("filename must be a string or TypedArray", .{});
|
|
};
|
|
|
|
var listener: jsc.JSValue = .zero;
|
|
var persistent: bool = true;
|
|
var bigint: bool = false;
|
|
var interval: i32 = 5007;
|
|
|
|
if (arguments.nextEat()) |options_or_callable| {
|
|
// options
|
|
if (options_or_callable.isObject()) {
|
|
// default true
|
|
persistent = (try options_or_callable.getBooleanStrict(global, "persistent")) orelse true;
|
|
|
|
// default false
|
|
bigint = (try options_or_callable.getBooleanStrict(global, "bigint")) orelse false;
|
|
|
|
if (try options_or_callable.get(global, "interval")) |interval_| {
|
|
if (!interval_.isNumber() and !interval_.isAnyInt()) {
|
|
return global.throwInvalidArguments("interval must be a number", .{});
|
|
}
|
|
interval = try interval_.coerce(i32, global);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (arguments.nextEat()) |listener_| {
|
|
if (listener_.isCallable()) {
|
|
listener = listener_.withAsyncContextIfNeeded(global);
|
|
}
|
|
}
|
|
|
|
if (listener == .zero) {
|
|
return global.throwInvalidArguments("Expected \"listener\" callback", .{});
|
|
}
|
|
|
|
return Arguments{
|
|
.path = path,
|
|
.listener = listener,
|
|
.persistent = persistent,
|
|
.bigint = bigint,
|
|
.interval = interval,
|
|
.global_this = global,
|
|
};
|
|
}
|
|
|
|
pub fn createStatWatcher(this: Arguments) !jsc.JSValue {
|
|
const obj = try StatWatcher.init(this);
|
|
if (obj.js_this != .zero) {
|
|
return obj.js_this;
|
|
}
|
|
return .js_undefined;
|
|
}
|
|
};
|
|
|
|
pub fn doRef(this: *StatWatcher, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
|
if (!this.closed and !this.persistent) {
|
|
this.persistent = true;
|
|
this.poll_ref.ref(this.ctx);
|
|
}
|
|
return .js_undefined;
|
|
}
|
|
|
|
pub fn doUnref(this: *StatWatcher, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
|
if (this.persistent) {
|
|
this.persistent = false;
|
|
this.poll_ref.unref(this.ctx);
|
|
}
|
|
return .js_undefined;
|
|
}
|
|
|
|
/// Stops file watching but does not free the instance.
|
|
pub fn close(this: *StatWatcher) void {
|
|
if (this.persistent) {
|
|
this.persistent = false;
|
|
this.poll_ref.unref(this.ctx);
|
|
}
|
|
this.closed = true;
|
|
this.last_jsvalue.clearWithoutDeallocation();
|
|
}
|
|
|
|
pub fn doClose(this: *StatWatcher, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
|
this.close();
|
|
return .js_undefined;
|
|
}
|
|
|
|
/// If the scheduler is not using this, free instantly, otherwise mark for being freed.
|
|
pub fn finalize(this: *StatWatcher) void {
|
|
log("Finalize\n", .{});
|
|
this.closed = true;
|
|
this.scheduler.deref();
|
|
this.deref(); // but don't deinit until the scheduler drops its reference
|
|
}
|
|
|
|
pub const InitialStatTask = struct {
|
|
watcher: *StatWatcher,
|
|
task: jsc.WorkPoolTask = .{ .callback = &workPoolCallback },
|
|
|
|
pub fn createAndSchedule(watcher: *StatWatcher) void {
|
|
const task = bun.new(InitialStatTask, .{ .watcher = watcher });
|
|
jsc.WorkPool.schedule(&task.task);
|
|
}
|
|
|
|
fn workPoolCallback(task: *jsc.WorkPoolTask) void {
|
|
const initial_stat_task: *InitialStatTask = @fieldParentPtr("task", task);
|
|
defer bun.destroy(initial_stat_task);
|
|
const this = initial_stat_task.watcher;
|
|
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
|
|
const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic))
|
|
bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })
|
|
else brk: {
|
|
const result = bun.sys.stat(this.path);
|
|
break :brk switch (result) {
|
|
.result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) },
|
|
.err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e },
|
|
};
|
|
};
|
|
switch (stat) {
|
|
.result => |res| {
|
|
// we store the stat, but do not call the callback
|
|
this.last_stat = res;
|
|
this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, initialStatSuccessOnMainThread));
|
|
},
|
|
.err => {
|
|
// on enoent, eperm, we call cb with two zeroed stat objects
|
|
// and store previous stat as a zeroed stat object, and then call the callback.
|
|
this.last_stat = std.mem.zeroes(bun.sys.PosixStat);
|
|
this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, initialStatErrorOnMainThread));
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn initialStatSuccessOnMainThread(this: *StatWatcher) void {
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
|
|
const jsvalue = statToJSStats(this.globalThis, &this.last_stat, this.bigint) catch return; // TODO: properly propagate exception upwards
|
|
this.last_jsvalue = .create(jsvalue, this.globalThis);
|
|
|
|
this.scheduler.data.append(this);
|
|
}
|
|
|
|
pub fn initialStatErrorOnMainThread(this: *StatWatcher) void {
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
|
|
const jsvalue = statToJSStats(this.globalThis, &this.last_stat, this.bigint) catch return; // TODO: properly propagate exception upwards
|
|
this.last_jsvalue = .create(jsvalue, this.globalThis);
|
|
|
|
_ = js.listenerGetCached(this.js_this).?.call(
|
|
this.globalThis,
|
|
.js_undefined,
|
|
&[2]jsc.JSValue{
|
|
jsvalue,
|
|
jsvalue,
|
|
},
|
|
) catch |err| this.globalThis.reportActiveExceptionAsUnhandled(err);
|
|
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
this.scheduler.data.append(this);
|
|
}
|
|
|
|
/// Called from any thread
|
|
pub fn restat(this: *StatWatcher) void {
|
|
log("recalling stat", .{});
|
|
const stat: bun.sys.Maybe(bun.sys.PosixStat) = if (bun.Environment.isLinux and bun.sys.supports_statx_on_linux.load(.monotonic))
|
|
bun.sys.statx(this.path, &.{ .type, .mode, .nlink, .uid, .gid, .atime, .mtime, .ctime, .btime, .ino, .size, .blocks })
|
|
else brk: {
|
|
const result = bun.sys.stat(this.path);
|
|
break :brk switch (result) {
|
|
.result => |r| bun.sys.Maybe(bun.sys.PosixStat){ .result = bun.sys.PosixStat.init(&r) },
|
|
.err => |e| bun.sys.Maybe(bun.sys.PosixStat){ .err = e },
|
|
};
|
|
};
|
|
const res = switch (stat) {
|
|
.result => |res| res,
|
|
.err => std.mem.zeroes(bun.sys.PosixStat),
|
|
};
|
|
|
|
// Ignore atime changes when comparing stats
|
|
// Compare field-by-field to avoid false positives from padding bytes
|
|
if (res.dev == this.last_stat.dev and
|
|
res.ino == this.last_stat.ino and
|
|
res.mode == this.last_stat.mode and
|
|
res.nlink == this.last_stat.nlink and
|
|
res.uid == this.last_stat.uid and
|
|
res.gid == this.last_stat.gid and
|
|
res.rdev == this.last_stat.rdev and
|
|
res.size == this.last_stat.size and
|
|
res.blksize == this.last_stat.blksize and
|
|
res.blocks == this.last_stat.blocks and
|
|
res.mtim.sec == this.last_stat.mtim.sec and
|
|
res.mtim.nsec == this.last_stat.mtim.nsec and
|
|
res.ctim.sec == this.last_stat.ctim.sec and
|
|
res.ctim.nsec == this.last_stat.ctim.nsec and
|
|
res.birthtim.sec == this.last_stat.birthtim.sec and
|
|
res.birthtim.nsec == this.last_stat.birthtim.nsec)
|
|
return;
|
|
|
|
this.last_stat = res;
|
|
this.enqueueTaskConcurrent(jsc.ConcurrentTask.fromCallback(this, swapAndCallListenerOnMainThread));
|
|
}
|
|
|
|
/// After a restat found the file changed, this calls the listener function.
|
|
pub fn swapAndCallListenerOnMainThread(this: *StatWatcher) void {
|
|
const prev_jsvalue = this.last_jsvalue.swap();
|
|
const current_jsvalue = statToJSStats(this.globalThis, &this.last_stat, this.bigint) catch return; // TODO: properly propagate exception upwards
|
|
this.last_jsvalue.set(this.globalThis, current_jsvalue);
|
|
|
|
_ = js.listenerGetCached(this.js_this).?.call(
|
|
this.globalThis,
|
|
.js_undefined,
|
|
&[2]jsc.JSValue{
|
|
current_jsvalue,
|
|
prev_jsvalue,
|
|
},
|
|
) catch |err| this.globalThis.reportActiveExceptionAsUnhandled(err);
|
|
}
|
|
|
|
pub fn init(args: Arguments) !*StatWatcher {
|
|
log("init", .{});
|
|
|
|
const buf = bun.path_buffer_pool.get();
|
|
defer bun.path_buffer_pool.put(buf);
|
|
var slice = args.path.slice();
|
|
if (bun.strings.startsWith(slice, "file://")) {
|
|
slice = slice[6..];
|
|
}
|
|
|
|
var parts = [_]string{slice};
|
|
const file_path = Path.joinAbsStringBuf(
|
|
fs.FileSystem.instance.top_level_dir,
|
|
buf,
|
|
&parts,
|
|
.auto,
|
|
);
|
|
|
|
const alloc_file_path = try bun.default_allocator.allocSentinel(u8, file_path.len, 0);
|
|
errdefer bun.default_allocator.free(alloc_file_path);
|
|
@memcpy(alloc_file_path, file_path);
|
|
|
|
var this = try bun.default_allocator.create(StatWatcher);
|
|
const vm = args.global_this.bunVM();
|
|
this.* = .{
|
|
.ctx = vm,
|
|
.persistent = args.persistent,
|
|
.bigint = args.bigint,
|
|
.interval = @max(5, args.interval),
|
|
.globalThis = args.global_this,
|
|
.js_this = .zero,
|
|
.closed = false,
|
|
.path = alloc_file_path,
|
|
// Instant.now will not fail on our target platforms.
|
|
.last_check = std.time.Instant.now() catch unreachable,
|
|
// InitStatTask is responsible for setting this
|
|
.last_stat = std.mem.zeroes(bun.sys.PosixStat),
|
|
.last_jsvalue = .empty,
|
|
.scheduler = vm.rareData().nodeFSStatWatcherScheduler(vm),
|
|
.ref_count = .init(),
|
|
};
|
|
errdefer this.deinit();
|
|
|
|
if (this.persistent) {
|
|
this.poll_ref.ref(this.ctx);
|
|
}
|
|
|
|
const js_this = StatWatcher.toJS(this, this.globalThis);
|
|
this.js_this = js_this;
|
|
js.listenerSetCached(js_this, this.globalThis, args.listener);
|
|
InitialStatTask.createAndSchedule(this);
|
|
|
|
return this;
|
|
}
|
|
};
|
|
|
|
const string = []const u8;
|
|
|
|
const Path = @import("../../resolver/resolve_path.zig");
|
|
const fs = @import("../../fs.zig");
|
|
const std = @import("std");
|
|
|
|
const bun = @import("bun");
|
|
const Output = bun.Output;
|
|
const UnboundedQueue = bun.threading.UnboundedQueue;
|
|
const EventLoopTimer = bun.api.Timer.EventLoopTimer;
|
|
|
|
const jsc = bun.jsc;
|
|
const EventLoop = jsc.EventLoop;
|
|
const VirtualMachine = jsc.VirtualMachine;
|
|
const ArgumentsSlice = jsc.CallFrame.ArgumentsSlice;
|
|
|
|
const PathLike = jsc.Node.PathLike;
|
|
const StatsBig = bun.jsc.Node.StatsBig;
|
|
const StatsSmall = bun.jsc.Node.StatsSmall;
|