aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bun.js/api/bun.zig6
-rw-r--r--src/bun.js/bindings/JSFFIFunction.cpp33
-rw-r--r--src/bun.js/bindings/bindings.zig41
-rw-r--r--src/bun.js/javascript.zig43
-rw-r--r--src/bun.js/test/jest.zig137
-rw-r--r--src/cli/test_command.zig5
-rw-r--r--test/bun.js/event-emitter.test.ts38
7 files changed, 284 insertions, 19 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index 52b457e54..727712897 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -2270,7 +2270,7 @@ pub const Timer = struct {
}
var this = args.ptr[1].asPtr(CallbackJob);
- globalThis.bunVM().runErrorHandler(args.ptr[0], null);
+ globalThis.bunVM().runErrorHandlerWithDedupe(args.ptr[0], null);
this.deinit();
return JSValue.jsUndefined();
}
@@ -2313,7 +2313,7 @@ pub const Timer = struct {
}
if (result.isAnyError(globalThis)) {
- vm.runErrorHandler(result, null);
+ vm.runErrorHandlerWithDedupe(result, null);
this.deinit();
return;
}
@@ -2322,7 +2322,7 @@ pub const Timer = struct {
switch (promise.status(globalThis.vm())) {
.Rejected => {
this.deinit();
- vm.runErrorHandler(promise.result(globalThis.vm()), null);
+ vm.runErrorHandlerWithDedupe(promise.result(globalThis.vm()), null);
},
.Fulfilled => {
this.deinit();
diff --git a/src/bun.js/bindings/JSFFIFunction.cpp b/src/bun.js/bindings/JSFFIFunction.cpp
index c0fc34fc0..a0cd83ba6 100644
--- a/src/bun.js/bindings/JSFFIFunction.cpp
+++ b/src/bun.js/bindings/JSFFIFunction.cpp
@@ -70,14 +70,45 @@ extern "C" FFICallbackFunctionWrapper* Bun__createFFICallbackFunction(
return wrapper;
}
-extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunction(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong)
+extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunctionWithData(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong, void* data)
{
JSC::VM& vm = globalObject->vm();
Zig::JSFFIFunction* function = Zig::JSFFIFunction::create(vm, globalObject, argCount, symbolName != nullptr ? Zig::toStringCopy(*symbolName) : String(), functionPointer, JSC::NoIntrinsic);
if (strong)
globalObject->trackFFIFunction(function);
+ function->dataPtr = data;
return function;
}
+
+extern "C" JSC::EncodedJSValue Bun__CreateFFIFunctionWithDataValue(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong, void* data)
+{
+ return JSC::JSValue::encode(Bun__CreateFFIFunctionWithData(globalObject, symbolName, argCount, functionPointer, strong, data));
+}
+
+extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunction(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, bool strong)
+{
+ return Bun__CreateFFIFunctionWithData(globalObject, symbolName, argCount, functionPointer, strong, nullptr);
+}
+
+extern "C" void* Bun__FFIFunction_getDataPtr(JSC::EncodedJSValue jsValue)
+{
+
+ Zig::JSFFIFunction* function = jsDynamicCast<Zig::JSFFIFunction*>(JSC::JSValue::decode(jsValue));
+ if (!function)
+ return nullptr;
+
+ return function->dataPtr;
+}
+
+extern "C" void Bun__FFIFunction_setDataPtr(JSC::EncodedJSValue jsValue, void* ptr)
+{
+
+ Zig::JSFFIFunction* function = jsDynamicCast<Zig::JSFFIFunction*>(JSC::JSValue::decode(jsValue));
+ if (!function)
+ return;
+
+ function->dataPtr = ptr;
+}
extern "C" void Bun__untrackFFIFunction(Zig::GlobalObject* globalObject, JSC::EncodedJSValue function)
{
globalObject->untrackFFIFunction(JSC::jsCast<JSC::JSFunction*>(JSC::JSValue::decode(function)));
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 76bb1011a..813a222ef 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -4217,6 +4217,14 @@ pub const JSArray = struct {
};
const private = struct {
+ pub extern fn Bun__CreateFFIFunctionWithDataValue(
+ *JSGlobalObject,
+ ?*const ZigString,
+ argCount: u32,
+ function: *const anyopaque,
+ strong: bool,
+ data: *anyopaque,
+ ) JSValue;
pub extern fn Bun__CreateFFIFunction(
globalObject: *JSGlobalObject,
symbolName: ?*const ZigString,
@@ -4237,7 +4245,11 @@ const private = struct {
globalObject: *JSGlobalObject,
function: JSValue,
) bool;
+
+ pub extern fn Bun__FFIFunction_getDataPtr(JSValue) ?*anyopaque;
+ pub extern fn Bun__FFIFunction_setDataPtr(JSValue, ?*anyopaque) void;
};
+
pub fn NewFunctionPtr(globalObject: *JSGlobalObject, symbolName: ?*const ZigString, argCount: u32, functionPointer: anytype, strong: bool) *anyopaque {
if (comptime JSC.is_bindgen) unreachable;
return private.Bun__CreateFFIFunction(globalObject, symbolName, argCount, @ptrCast(*const anyopaque, functionPointer), strong);
@@ -4254,6 +4266,35 @@ pub fn NewFunction(
return private.Bun__CreateFFIFunctionValue(globalObject, symbolName, argCount, @ptrCast(*const anyopaque, functionPointer), strong);
}
+pub fn getFunctionData(function: JSValue) ?*anyopaque {
+ if (comptime JSC.is_bindgen) unreachable;
+ return private.Bun__FFIFunction_getDataPtr(function);
+}
+
+pub fn setFunctionData(function: JSValue, value: ?*anyopaque) void {
+ if (comptime JSC.is_bindgen) unreachable;
+ return private.Bun__FFIFunction_setDataPtr(function, value);
+}
+
+pub fn NewFunctionWithData(
+ globalObject: *JSGlobalObject,
+ symbolName: ?*const ZigString,
+ argCount: u32,
+ functionPointer: anytype,
+ strong: bool,
+ data: *anyopaque,
+) JSValue {
+ if (comptime JSC.is_bindgen) unreachable;
+ return private.Bun__CreateFFIFunctionWithDataValue(
+ globalObject,
+ symbolName,
+ argCount,
+ @ptrCast(*const anyopaque, functionPointer),
+ strong,
+ data,
+ );
+}
+
pub fn untrackFunction(
globalObject: *JSGlobalObject,
value: JSValue,
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index bbc10291c..1fd6783be 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -282,6 +282,7 @@ comptime {
_ = Bun__readOriginTimer;
_ = Bun__onDidAppendPlugin;
_ = Bun__readOriginTimerStart;
+ _ = Bun__reportUnhandledError;
}
}
@@ -291,6 +292,12 @@ pub export fn Bun__queueTask(global: *JSGlobalObject, task: *JSC.CppTask) void {
global.bunVM().eventLoop().enqueueTask(Task.init(task));
}
+pub export fn Bun__reportUnhandledError(globalObject: *JSGlobalObject, value: JSValue) callconv(.C) JSValue {
+ var jsc_vm = globalObject.bunVM();
+ jsc_vm.onUnhandledError(globalObject, value);
+ return JSC.JSValue.jsUndefined();
+}
+
/// This function is called on another thread
/// The main difference: we need to allocate the task & wakeup the thread
/// We can avoid that if we run it from the main thread.
@@ -305,7 +312,8 @@ pub export fn Bun__queueTaskConcurrently(global: *JSGlobalObject, task: *JSC.Cpp
pub export fn Bun__handleRejectedPromise(global: *JSGlobalObject, promise: *JSC.JSPromise) void {
const result = promise.result(global.vm());
- global.bunVM().runErrorHandler(result, null);
+ var jsc_vm = global.bunVM();
+ jsc_vm.onUnhandledError(global, result);
}
pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSGlobalObject) void {
@@ -350,6 +358,7 @@ pub const VirtualMachine = struct {
plugin_runner: ?PluginRunner = null,
is_main_thread: bool = false,
+ last_reported_error_for_dedupe: JSValue = .zero,
/// Do not access this field directly
/// It exists in the VirtualMachine struct so that
@@ -411,10 +420,27 @@ pub const VirtualMachine = struct {
auto_install_dependencies: bool = false,
load_builtins_from_path: []const u8 = "",
+ onUnhandledRejection: fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void = defaultOnUnhandledRejection,
+ onUnhandledRejectionCtx: ?*anyopaque = null,
+ unhandled_error_counter: usize = 0,
+
modules: ModuleLoader.AsyncModule.Queue = .{},
pub threadlocal var is_main_thread_vm: bool = false;
+ pub fn resetUnhandledRejection(this: *VirtualMachine) void {
+ this.onUnhandledRejection = defaultOnUnhandledRejection;
+ }
+
+ pub fn onUnhandledError(this: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ this.unhandled_error_counter += 1;
+ this.onUnhandledRejection(this, globalObject, value);
+ }
+
+ pub fn defaultOnUnhandledRejection(this: *JSC.VirtualMachine, _: *JSC.JSGlobalObject, value: JSC.JSValue) void {
+ this.runErrorHandler(value, null);
+ }
+
pub inline fn packageManager(this: *VirtualMachine) *PackageManager {
return this.bundler.getPackageManager();
}
@@ -1173,7 +1199,17 @@ pub const VirtualMachine = struct {
}
}
+ pub fn runErrorHandlerWithDedupe(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void {
+ if (this.last_reported_error_for_dedupe == result and !this.last_reported_error_for_dedupe.isEmptyOrUndefinedOrNull())
+ return;
+
+ this.runErrorHandler(result, exception_list);
+ }
+
pub fn runErrorHandler(this: *VirtualMachine, result: JSValue, exception_list: ?*ExceptionList) void {
+ if (!result.isEmptyOrUndefinedOrNull())
+ this.last_reported_error_for_dedupe = result;
+
if (result.isException(this.global.vm())) {
var exception = @ptrCast(*Exception, result.asVoid());
@@ -1454,8 +1490,9 @@ pub const VirtualMachine = struct {
}
}
- pub fn reportUncaughtExceptio(_: *JSGlobalObject, exception: *JSC.Exception) JSValue {
- VirtualMachine.vm.runErrorHandler(exception.value(), null);
+ pub fn reportUncaughtException(globalObject: *JSGlobalObject, exception: *JSC.Exception) JSValue {
+ var jsc_vm = globalObject.bunVM();
+ jsc_vm.onUnhandledError(globalObject, exception.value());
return JSC.JSValue.jsUndefined();
}
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index 7440a9512..8674e2c22 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const bun = @import("../../global.zig");
const Api = @import("../../api/schema.zig").Api;
const RequestContext = @import("../../http.zig").RequestContext;
const MimeType = @import("../../http.zig").MimeType;
@@ -535,14 +536,36 @@ pub const TestScope = struct {
const err = arguments.ptr[0];
globalThis.bunVM().runErrorHandler(err, null);
var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask);
- task.handleResult(.{ .fail = active_test_expectation_counter.actual });
+ task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .promise);
return JSValue.jsUndefined();
}
pub fn onResolve(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
const arguments = callframe.arguments(2);
var task: *TestRunnerTask = arguments.ptr[1].asPromisePtr(TestRunnerTask);
- task.handleResult(.{ .pass = active_test_expectation_counter.actual });
+ task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .promise);
+ return JSValue.jsUndefined();
+ }
+
+ pub fn onDone(
+ globalThis: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSValue {
+ const function = callframe.callee();
+ const args = callframe.arguments(1);
+
+ if (JSC.getFunctionData(function)) |data| {
+ var task = bun.cast(*TestRunnerTask, data);
+ JSC.setFunctionData(function, null);
+ if (args.len > 0) {
+ const err = args.ptr[0];
+ globalThis.bunVM().runErrorHandlerWithDedupe(err, null);
+ task.handleResult(.{ .fail = active_test_expectation_counter.actual }, .callback);
+ } else {
+ task.handleResult(.{ .pass = active_test_expectation_counter.actual }, .callback);
+ }
+ }
+
return JSValue.jsUndefined();
}
@@ -558,7 +581,24 @@ pub const TestScope = struct {
this.callback = null;
}
JSC.markBinding(@src());
- const initial_value = js.JSObjectCallAsFunctionReturnValue(vm.global, callback, null, 0, null);
+
+ const callback_length = JSValue.fromRef(callback).getLengthOfArray(vm.global);
+
+ var initial_value = JSValue.zero;
+ if (callback_length > 0) {
+ const callback_func = JSC.NewFunctionWithData(
+ vm.global,
+ ZigString.static("done"),
+ 0,
+ TestScope.onDone,
+ false,
+ task,
+ );
+ task.done_callback_state = .pending;
+ initial_value = JSValue.fromRef(callback.?).call(vm.global, &.{callback_func});
+ } else {
+ initial_value = js.JSObjectCallAsFunctionReturnValue(vm.global, callback, null, 0, null);
+ }
if (initial_value.isException(vm.global.vm()) or initial_value.isError() or initial_value.isAggregateError(vm.global)) {
vm.runErrorHandler(initial_value, null);
@@ -579,6 +619,7 @@ pub const TestScope = struct {
return .{ .fail = active_test_expectation_counter.actual };
},
.Pending => {
+ task.promise_state = .pending;
_ = promise.asValue(vm.global).then(vm.global, task, onResolve, onReject);
return .{ .pending = {} };
},
@@ -589,6 +630,10 @@ pub const TestScope = struct {
}
}
+ if (callback_length > 0) {
+ return .{ .pending = {} };
+ }
+
this.callback = null;
if (active_test_expectation_counter.expected > 0 and active_test_expectation_counter.expected < active_test_expectation_counter.actual) {
@@ -960,7 +1005,7 @@ pub const DescribeScope = struct {
var active_test_expectation_counter: TestScope.Counter = undefined;
-const TestRunnerTask = struct {
+pub const TestRunnerTask = struct {
test_id: TestRunner.Test.ID,
describe: *DescribeScope,
globalThis: *JSC.JSGlobalObject,
@@ -969,6 +1014,31 @@ const TestRunnerTask = struct {
needs_before_each: bool = true,
ref: JSC.Ref = JSC.Ref.init(),
+ done_callback_state: AsyncState = .none,
+ promise_state: AsyncState = .none,
+ sync_state: AsyncState = .none,
+ reported: bool = false,
+
+ pub const AsyncState = enum {
+ none,
+ pending,
+ fulfilled,
+ };
+
+ pub fn onUnhandledRejection(jsc_vm: *VirtualMachine, _: *JSC.JSGlobalObject, rejection: JSC.JSValue) void {
+ if (jsc_vm.last_reported_error_for_dedupe == rejection and rejection != .zero) {
+ jsc_vm.last_reported_error_for_dedupe = .zero;
+ } else {
+ jsc_vm.runErrorHandlerWithDedupe(rejection, null);
+ }
+
+ if (jsc_vm.onUnhandledRejectionCtx) |ctx| {
+ var this = bun.cast(*TestRunnerTask, ctx);
+ jsc_vm.onUnhandledRejectionCtx = null;
+ this.handleResult(.{ .fail = active_test_expectation_counter.actual }, .unhandledRejection);
+ }
+ }
+
pub fn run(this: *TestRunnerTask) bool {
var describe = this.describe;
@@ -977,7 +1047,9 @@ const TestRunnerTask = struct {
DescribeScope.active = describe;
active_test_expectation_counter = .{};
+ describe.current_test_id = this.test_id;
var globalThis = this.globalThis;
+ globalThis.bunVM().onUnhandledRejectionCtx = this;
var test_: TestScope = this.describe.tests.items[this.test_id];
const label = this.describe.tests.items[this.test_id].label;
@@ -995,19 +1067,56 @@ const TestRunnerTask = struct {
}
}
- const result = TestScope.run(&test_, this);
+ this.sync_state = .pending;
- if (result == .pending) {
+ const result = TestScope.run(&test_, this);
+ if (result == .pending and this.sync_state == .pending and (this.done_callback_state == .pending or this.promise_state == .pending)) {
+ this.sync_state = .fulfilled;
this.value.set(globalThis, this.describe.value);
return true;
}
- this.handleResult(result);
+ this.handleResult(result, .sync);
return false;
}
- pub fn handleResult(this: *TestRunnerTask, result: Result) void {
+ pub fn handleResult(this: *TestRunnerTask, result: Result, comptime from: @Type(.EnumLiteral)) void {
+ switch (comptime from) {
+ .promise => {
+ std.debug.assert(this.promise_state == .pending);
+ this.promise_state = .fulfilled;
+
+ if (this.done_callback_state == .pending and result == .pass) {
+ return;
+ }
+ },
+ .callback => {
+ std.debug.assert(this.done_callback_state == .pending);
+ this.done_callback_state = .fulfilled;
+
+ if (this.promise_state == .pending and result == .pass) {
+ return;
+ }
+ },
+ .sync => {
+ std.debug.assert(this.sync_state == .pending);
+ this.sync_state = .fulfilled;
+ },
+ .unhandledRejection => {},
+ else => @compileError("Bad from"),
+ }
+
+ defer {
+ if (this.reported and this.promise_state != .pending and this.sync_state != .pending and this.done_callback_state != .pending)
+ this.deinit();
+ }
+
+ if (this.reported)
+ return;
+
+ this.reported = true;
+
var globalThis = this.globalThis;
var test_ = this.describe.tests.items[this.test_id];
const label = this.describe.tests.items[this.test_id].label;
@@ -1015,20 +1124,26 @@ const TestRunnerTask = struct {
var describe = this.describe;
describe.tests.items[this.test_id] = test_;
-
switch (result) {
.pass => |count| Jest.runner.?.reportPass(test_id, this.source.path.text, label, count, describe),
.fail => |count| Jest.runner.?.reportFailure(test_id, this.source.path.text, label, count, describe),
.pending => @panic("Unexpected pending test"),
}
describe.onTestComplete(globalThis, this.test_id);
- this.deinit();
+
Jest.runner.?.runNextTest();
}
fn deinit(this: *TestRunnerTask) void {
+ var vm = JSC.VirtualMachine.vm;
+ if (vm.onUnhandledRejectionCtx) |ctx| {
+ if (ctx == @ptrCast(*anyopaque, this)) {
+ vm.onUnhandledRejectionCtx = null;
+ }
+ }
+
this.value.deinit();
- this.ref.unref(JSC.VirtualMachine.vm);
+ this.ref.unref(vm);
default_allocator.destroy(this);
}
};
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
index 1927e6027..7b5c88947 100644
--- a/src/cli/test_command.zig
+++ b/src/cli/test_command.zig
@@ -482,10 +482,13 @@ pub const TestCommand = struct {
var modules: []*Jest.DescribeScope = reporter.jest.files.items(.module_scope)[file_start..];
for (modules) |module| {
+ vm.onUnhandledRejectionCtx = null;
+ vm.onUnhandledRejection = Jest.TestRunnerTask.onUnhandledRejection;
module.runTests(JSC.JSValue.zero, vm.global);
vm.eventLoop().tick();
- while (vm.active_tasks > 0) {
+ const initial_unhandled_counter = vm.unhandled_error_counter;
+ while (vm.active_tasks > 0 and vm.unhandled_error_counter == initial_unhandled_counter) {
if (!Jest.Jest.runner.?.has_pending_tests) Jest.Jest.runner.?.drain();
vm.eventLoop().tick();
diff --git a/test/bun.js/event-emitter.test.ts b/test/bun.js/event-emitter.test.ts
index 4c8d70452..952805cf8 100644
--- a/test/bun.js/event-emitter.test.ts
+++ b/test/bun.js/event-emitter.test.ts
@@ -13,4 +13,42 @@ describe("EventEmitter", () => {
emitter.setMaxListeners(100);
expect(emitter.getMaxListeners()).toBe(100);
});
+
+ // These are also tests for the done() function in the test runner.
+ test("EventEmitter emit (different tick)", (done) => {
+ var emitter = new EventEmitter();
+ emitter.on("wow", () => done());
+ queueMicrotask(() => {
+ emitter.emit("wow");
+ });
+ });
+
+ // Unlike Jest, bun supports async and done
+ test("async EventEmitter emit (microtask)", async (done) => {
+ await 1;
+ var emitter = new EventEmitter();
+ emitter.on("wow", () => done());
+ emitter.emit("wow");
+ });
+
+ test("async EventEmitter emit (microtask) after", async (done) => {
+ var emitter = new EventEmitter();
+ emitter.on("wow", () => done());
+ await 1;
+ emitter.emit("wow");
+ });
+
+ test("EventEmitter emit (same tick)", (done) => {
+ var emitter = new EventEmitter();
+
+ emitter.on("wow", () => done());
+
+ emitter.emit("wow");
+ });
+
+ test("EventEmitter emit (setTimeout task)", (done) => {
+ var emitter = new EventEmitter();
+ emitter.on("wow", () => done());
+ setTimeout(() => emitter.emit("wow"), 1);
+ });
});