diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 95a491ae413356..7c35e1cbb1546e 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -358,6 +358,7 @@ src/codegen/process_windows_translate_c.zig src/compile_target.zig src/comptime_string_map.zig src/copy_file.zig +src/cpp.zig src/crash_handler.zig src/create/SourceFileProjectGenerator.zig src/csrf.zig diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 5ec6dbcf6da269..00933e35886ef4 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1879,7 +1879,10 @@ pub const Formatter = struct { .estimated_line_length = &this.formatter.estimated_line_length, }; - if (getObjectName(globalThis, value)) |name_str| { + if (getObjectName(globalThis, value) catch blk: { + globalThis.clearException(); + break :blk null; + }) |name_str| { writer.print("{} ", .{name_str}); } } @@ -2023,9 +2026,9 @@ pub const Formatter = struct { }; } - fn getObjectName(globalThis: *JSC.JSGlobalObject, value: JSValue) ?ZigString { + fn getObjectName(globalThis: *JSC.JSGlobalObject, value: JSValue) bun.JSError!?ZigString { var name_str = ZigString.init(""); - value.getClassName(globalThis, &name_str); + try value.getClassName(globalThis, &name_str); if (!name_str.eqlComptime("Object")) { return name_str; } else if (value.getPrototype(globalThis).eqlValue(JSValue.null)) { @@ -2198,7 +2201,7 @@ pub const Formatter = struct { .Double => { if (value.isCell()) { var number_name = ZigString.Empty; - value.getClassName(this.globalThis, &number_name); + try value.getClassName(this.globalThis, &number_name); var number_value = ZigString.Empty; try value.toZigString(&number_value, this.globalThis); @@ -2289,12 +2292,12 @@ pub const Formatter = struct { }, .Class => { var printable = ZigString.init(&name_buf); - value.getClassName(this.globalThis, &printable); + try value.getClassName(this.globalThis, &printable); this.addForNewLine(printable.len); const proto = value.getPrototype(this.globalThis); var printable_proto = ZigString.init(&name_buf); - proto.getClassName(this.globalThis, &printable_proto); + try proto.getClassName(this.globalThis, &printable_proto); this.addForNewLine(printable_proto.len); if (printable.len == 0) { @@ -2622,7 +2625,7 @@ pub const Formatter = struct { } else if (value.as(bun.api.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (JestPrettyFormat.printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) { + } else if (try JestPrettyFormat.printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) { return; } else if (jsType != .DOMWrapper) { if (value.isCallable()) { @@ -2663,7 +2666,7 @@ pub const Formatter = struct { .Boolean => { if (value.isCell()) { var bool_name = ZigString.Empty; - value.getClassName(this.globalThis, &bool_name); + try value.getClassName(this.globalThis, &bool_name); var bool_value = ZigString.Empty; try value.toZigString(&bool_value, this.globalThis); @@ -3305,7 +3308,7 @@ pub const Formatter = struct { else if (value.isCallable()) try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors) else { - if (getObjectName(this.globalThis, value)) |name_str| { + if (try getObjectName(this.globalThis, value)) |name_str| { writer.print("{} ", .{name_str}); } writer.writeAll("{}"); diff --git a/src/bun.js/bindings/CatchScope.zig b/src/bun.js/bindings/CatchScope.zig index 2d07c70ddd82e1..9170864b4bfcd3 100644 --- a/src/bun.js/bindings/CatchScope.zig +++ b/src/bun.js/bindings/CatchScope.zig @@ -138,6 +138,13 @@ pub fn assertNoExceptionExceptTermination(self: *CatchScope) bun.JSExecutionTerm } } +/// Clear the thrown exception +pub fn clearException(self: *CatchScope) void { + if (Environment.allow_assert) bun.assert(self.location == &self.bytes[0]); + if (!self.enabled) return; + CatchScope__clearException(&self.bytes); +} + pub fn deinit(self: *CatchScope) void { if (comptime Environment.ci_assert) bun.assert(self.location == &self.bytes[0]); if (!self.enabled) return; @@ -161,6 +168,7 @@ extern fn CatchScope__pureException(ptr: *align(alignment) [size]u8) ?*jsc.Excep extern fn CatchScope__exceptionIncludingTraps(ptr: *align(alignment) [size]u8) ?*jsc.Exception; extern fn CatchScope__assertNoException(ptr: *align(alignment) [size]u8) void; extern fn CatchScope__destruct(ptr: *align(alignment) [size]u8) void; +extern fn CatchScope__clearException(ptr: *align(alignment) [size]u8) void; const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.js/bindings/CatchScopeBinding.cpp b/src/bun.js/bindings/CatchScopeBinding.cpp index 3efe668735f3da..ac163310560010 100644 --- a/src/bun.js/bindings/CatchScopeBinding.cpp +++ b/src/bun.js/bindings/CatchScopeBinding.cpp @@ -55,3 +55,9 @@ extern "C" void CatchScope__assertNoException(void* ptr) ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); static_cast(ptr)->assertNoException(); } + +extern "C" void CatchScope__clearException(void* ptr) +{ + ASSERT((uintptr_t)ptr % alignof(CatchScope) == 0); + static_cast(ptr)->clearException(); +} diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 2407342255bedd..67da2fc8b3a973 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -703,9 +703,9 @@ pub const JSValue = enum(i64) { return JSC__JSValue__jsTDZValue(); } - pub fn className(this: JSValue, globalThis: *JSGlobalObject) ZigString { + pub fn className(this: JSValue, globalThis: *JSGlobalObject) JSError!ZigString { var str = ZigString.init(""); - this.getClassName(globalThis, &str); + try this.getClassName(globalThis, &str); return str; } @@ -1095,8 +1095,8 @@ pub const JSValue = enum(i64) { } extern fn JSC__JSValue__getClassName(this: JSValue, global: *JSGlobalObject, ret: *ZigString) void; - pub fn getClassName(this: JSValue, global: *JSGlobalObject, ret: *ZigString) void { - JSC__JSValue__getClassName(this, global, ret); + pub fn getClassName(this: JSValue, global: *JSGlobalObject, ret: *ZigString) bun.JSError!void { + try JSC.fromJSHostCallGeneric(global, @src(), JSC__JSValue__getClassName, .{ this, global, ret }); } pub inline fn isCell(this: JSValue) bool { diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9dc722cb57bdab..3b1c81524d6713 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3201,7 +3201,7 @@ void GlobalObject::finishCreation(VM& vm) [](const JSC::LazyProperty::Initializer& init) { auto* global = init.owner; auto& vm = init.vm; - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); // if we get the termination exception, we'd still like to set a non-null Map so that // we don't segfault diff --git a/src/bun.js/bindings/webcore/JSAbortController.cpp b/src/bun.js/bindings/webcore/JSAbortController.cpp index 14f2e4f3782c22..631db6fcdf4042 100644 --- a/src/bun.js/bindings/webcore/JSAbortController.cpp +++ b/src/bun.js/bindings/webcore/JSAbortController.cpp @@ -48,6 +48,7 @@ #include #include #include +#include "ErrorCode.h" #include namespace WebCore { @@ -56,6 +57,7 @@ using namespace JSC; // Functions static JSC_DECLARE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_abort); +static JSC_DECLARE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_customInspect); // Attributes @@ -149,6 +151,7 @@ void JSAbortControllerPrototype::finishCreation(VM& vm) { Base::finishCreation(vm); reifyStaticProperties(vm, JSAbortController::info(), JSAbortControllerPrototypeTableValues, *this); + this->putDirectNativeFunction(vm, this->globalObject(), builtinNames(vm).inspectCustomPublicName(), 2, jsAbortControllerPrototypeFunction_customInspect, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::Function | 0); JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } @@ -225,6 +228,78 @@ JSC_DEFINE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_abort, (JSGlobalObje return IDLOperation::call(*lexicalGlobalObject, *callFrame, "abort"); } +static inline JSC::EncodedJSValue jsAbortControllerPrototypeFunction_customInspectBody(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + + auto& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + + JSValue depthValue = callFrame->argument(0); + JSValue optionsValue = callFrame->argument(1); + + auto depth = depthValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + if (depth < 0) { + return JSValue::encode(jsNontrivialString(vm, "[AbortController]"_s)); + } + + if (!depthValue.isUndefinedOrNull()) { + depthValue = jsNumber(depth - 1); + } + + JSObject* options = optionsValue.toObject(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + PropertyNameArray optionsArray(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); + options->getPropertyNames(lexicalGlobalObject, optionsArray, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(throwScope, {}); + + JSObject* newOptions = constructEmptyObject(lexicalGlobalObject); + for (size_t i = 0; i < optionsArray.size(); i++) { + auto name = optionsArray[i]; + + JSValue value = options->get(lexicalGlobalObject, name); + RETURN_IF_EXCEPTION(throwScope, {}); + + newOptions->putDirect(vm, name, value, 0); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + PutPropertySlot slot(newOptions); + newOptions->put(newOptions, lexicalGlobalObject, Identifier::fromString(vm, "depth"_s), depthValue, slot); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto& impl = castedThis->wrapped(); + + JSObject* inputObj = constructEmptyObject(lexicalGlobalObject); + + inputObj->putDirect(vm, Identifier::fromString(vm, "signal"_s), toJS>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, impl.signal()), 0); + + JSFunction* utilInspect = globalObject->utilInspectFunction(); + auto callData = JSC::getCallData(utilInspect); + MarkedArgumentBuffer arguments; + arguments.append(inputObj); + arguments.append(newOptions); + + auto inspectResult = JSC::profiledCall(globalObject, ProfilingReason::API, utilInspect, callData, inputObj, arguments); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto* inspectString = inspectResult.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto inspectStringView = inspectString->view(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + JSValue result = jsString(vm, makeString("AbortController "_s, inspectStringView.data)); + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_customInspect, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "inspect"); +} + JSC::GCClient::IsoSubspace* JSAbortController::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl( @@ -242,6 +317,7 @@ void JSAbortController::visitChildrenImpl(JSCell* cell, Visitor& visitor) ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); addWebCoreOpaqueRoot(visitor, thisObject->wrapped().opaqueRoot()); + thisObject->wrapped().signal().reason().visit(visitor); } DEFINE_VISIT_CHILDREN(JSAbortController); diff --git a/src/bun.js/bindings/webcore/JSAbortSignal.cpp b/src/bun.js/bindings/webcore/JSAbortSignal.cpp index 453f3376758a18..3ee7d97ea7732a 100644 --- a/src/bun.js/bindings/webcore/JSAbortSignal.cpp +++ b/src/bun.js/bindings/webcore/JSAbortSignal.cpp @@ -52,6 +52,7 @@ #include #include #include +#include "ErrorCode.h" #include #include "ErrorCode.h" @@ -64,6 +65,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_abort); static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_timeout); static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_any); static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_throwIfAborted); +static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_customInspect); // Attributes @@ -160,6 +162,7 @@ void JSAbortSignalPrototype::finishCreation(VM& vm) { Base::finishCreation(vm); reifyStaticProperties(vm, JSAbortSignal::info(), JSAbortSignalPrototypeTableValues, *this); + this->putDirectNativeFunction(vm, this->globalObject(), builtinNames(vm).inspectCustomPublicName(), 2, jsAbortSignalPrototypeFunction_customInspect, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::Function | 0); JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } @@ -354,6 +357,78 @@ JSC_DEFINE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_throwIfAborted, (JSGloba return IDLOperation::call(*lexicalGlobalObject, *callFrame, "throwIfAborted"); } +static inline JSC::EncodedJSValue jsAbortSignalPrototypeFunction_customInspectBody(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + + auto& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + + JSValue depthValue = callFrame->argument(0); + JSValue optionsValue = callFrame->argument(1); + + auto depth = depthValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + if (depth < 0) { + return JSValue::encode(jsNontrivialString(vm, "[AbortSignal]"_s)); + } + + if (!depthValue.isUndefinedOrNull()) { + depthValue = jsNumber(depth - 1); + } + + JSObject* options = optionsValue.toObject(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + PropertyNameArray optionsArray(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); + options->getPropertyNames(lexicalGlobalObject, optionsArray, DontEnumPropertiesMode::Exclude); + RETURN_IF_EXCEPTION(throwScope, {}); + + JSObject* newOptions = constructEmptyObject(lexicalGlobalObject); + for (size_t i = 0; i < optionsArray.size(); i++) { + auto name = optionsArray[i]; + + JSValue value = options->get(lexicalGlobalObject, name); + RETURN_IF_EXCEPTION(throwScope, {}); + + newOptions->putDirect(vm, name, value, 0); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + PutPropertySlot slot(newOptions); + newOptions->put(newOptions, lexicalGlobalObject, Identifier::fromString(vm, "depth"_s), depthValue, slot); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto& impl = castedThis->wrapped(); + + JSObject* inputObj = constructEmptyObject(lexicalGlobalObject); + + inputObj->putDirect(vm, Identifier::fromString(vm, "aborted"_s), jsBoolean(impl.aborted()), 0); + + JSFunction* utilInspect = globalObject->utilInspectFunction(); + auto callData = JSC::getCallData(utilInspect); + MarkedArgumentBuffer arguments; + arguments.append(inputObj); + arguments.append(newOptions); + + auto inspectResult = JSC::profiledCall(globalObject, ProfilingReason::API, utilInspect, callData, inputObj, arguments); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto* inspectString = inspectResult.toString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto inspectStringView = inspectString->view(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + JSValue result = jsString(vm, makeString("AbortSignal "_s, inspectStringView.data)); + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_customInspect, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "inspect"); +} + size_t JSAbortSignal::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) { auto* thisObject = jsCast(cell); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertNumbers.cpp b/src/bun.js/bindings/webcore/JSDOMConvertNumbers.cpp index d1c20179f35146..806c3ae62fc121 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertNumbers.cpp +++ b/src/bun.js/bindings/webcore/JSDOMConvertNumbers.cpp @@ -131,7 +131,7 @@ static inline T toSmallerInt(JSGlobalObject& lexicalGlobalObject, JSValue value) case IntegerConversionConfiguration::Normal: break; case IntegerConversionConfiguration::EnforceRange: - return enforceRange(lexicalGlobalObject, x, LimitsTrait::minValue, LimitsTrait::maxValue); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, LimitsTrait::minValue, LimitsTrait::maxValue)); case IntegerConversionConfiguration::Clamp: return std::isnan(x) ? 0 : clampTo(x); } @@ -177,7 +177,7 @@ static inline T toSmallerUInt(JSGlobalObject& lexicalGlobalObject, JSValue value case IntegerConversionConfiguration::Normal: break; case IntegerConversionConfiguration::EnforceRange: - return enforceRange(lexicalGlobalObject, x, 0, LimitsTrait::maxValue); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, LimitsTrait::maxValue)); case IntegerConversionConfiguration::Clamp: return std::isnan(x) ? 0 : clampTo(x); } @@ -262,7 +262,7 @@ template<> int32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& le double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, kMinInt32, kMaxInt32); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, kMinInt32, kMaxInt32)); } template<> uint32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -275,7 +275,7 @@ template<> uint32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, 0, kMaxUInt32); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, kMaxUInt32)); } template<> int32_t convertToIntegerClamp(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -316,7 +316,7 @@ template<> int64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& le double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, -kJSMaxInteger, kJSMaxInteger); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, -kJSMaxInteger, kJSMaxInteger)); } template<> uint64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -329,7 +329,7 @@ template<> uint64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, 0, kJSMaxInteger); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, kJSMaxInteger)); } template<> int64_t convertToIntegerClamp(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) diff --git a/src/bun.js/bindings/webcore/JSEvent.cpp b/src/bun.js/bindings/webcore/JSEvent.cpp index 1a59d7200fc395..5b114bb96ac087 100644 --- a/src/bun.js/bindings/webcore/JSEvent.cpp +++ b/src/bun.js/bindings/webcore/JSEvent.cpp @@ -122,18 +122,6 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSEventPrototype, JSEventPrototype::Base); using JSEventDOMConstructor = JSDOMConstructor; -/* Hash table */ - -static const struct CompactHashIndex JSEventTableIndex[2] = { - { 0, -1 }, - { -1, -1 }, -}; - -static const HashTableValue JSEventTableValues[] = { - { "isTrusted"_s, static_cast(JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_isTrusted, 0 } }, -}; - -static const HashTable JSEventTable = { 1, 1, true, JSEvent::info(), JSEventTableValues, JSEventTableIndex }; /* Hash table for constructor */ static const HashTableValue JSEventConstructorTableValues[] = { @@ -211,6 +199,8 @@ static const HashTableValue JSEventPrototypeTableValues[] = { { "bubbles"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_bubbles, 0 } }, { "cancelable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_cancelable, 0 } }, { "defaultPrevented"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_defaultPrevented, 0 } }, + // Node puts isTrusted on the prototype rather than the instance (what the web standard says). See node lib/internal/event_target.js isTrusted. + { "isTrusted"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_isTrusted, 0 } }, { "composed"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_composed, 0 } }, { "timeStamp"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_timeStamp, 0 } }, { "srcElement"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsEvent_srcElement, 0 } }, @@ -235,7 +225,7 @@ void JSEventPrototype::finishCreation(VM& vm) JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } -const ClassInfo JSEvent::s_info = { "Event"_s, &Base::s_info, &JSEventTable +const ClassInfo JSEvent::s_info = { "Event"_s, &Base::s_info, nullptr #if 0 , &checkSubClassSnippetForJSEvent diff --git a/src/bun.js/bindings/webcore/JSEvent.h b/src/bun.js/bindings/webcore/JSEvent.h index 77f324eb94e121..d01eab3cc64999 100644 --- a/src/bun.js/bindings/webcore/JSEvent.h +++ b/src/bun.js/bindings/webcore/JSEvent.h @@ -59,9 +59,6 @@ class JSEvent : public JSDOMWrapper { static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); -public: - static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::HasStaticPropertyTable; - protected: JSEvent(JSC::Structure*, JSDOMGlobalObject&, Ref&&); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 6521562fd8713b..5d2953b41a7971 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2286,7 +2286,7 @@ pub const Expect = struct { if (!result.isInstanceOf(globalThis, expected_value)) return .js_undefined; var expected_class = ZigString.Empty; - expected_value.getClassName(globalThis, &expected_class); + try expected_value.getClassName(globalThis, &expected_class); const received_message: JSValue = (try result.fastGet(globalThis, .message)) orelse .js_undefined; return this.throw(globalThis, signature, "\n\nExpected constructor: not {s}\n\nReceived message: {any}\n", .{ expected_class, received_message.toFmt(&formatter) }); } @@ -2410,8 +2410,12 @@ pub const Expect = struct { defer formatter.deinit(); var expected_class = ZigString.Empty; var received_class = ZigString.Empty; - expected_value.getClassName(globalThis, &expected_class); - result.getClassName(globalThis, &received_class); + try expected_value.getClassName(globalThis, &expected_class); + if (result.isCell()) { + try result.getClassName(globalThis, &received_class); + } else { + received_class = ZigString.init("primitive value"); + } const signature = comptime getSignature("toThrow", "expected", false); const fmt = signature ++ "\n\nExpected constructor: {s}\nReceived constructor: {s}\n\n"; @@ -2466,7 +2470,7 @@ pub const Expect = struct { const expected_fmt = "\n\nExpected constructor: {s}\n\n" ++ received_line; var expected_class = ZigString.Empty; - expected_value.getClassName(globalThis, &expected_class); + try expected_value.getClassName(globalThis, &expected_class); return this.throw(globalThis, signature, expected_fmt, .{ expected_class, result.toFmt(&formatter) }); } fn getValueAsToThrow(this: *Expect, globalThis: *JSGlobalObject, value: JSValue) bun.JSError!struct { ?JSValue, JSValue } { diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index db590730f14ea0..032a66aeff057e 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1758,10 +1758,10 @@ inline fn createScope( } if (description.isClass(globalThis)) { - const name_str = if (description.className(globalThis).toSlice(allocator).length() == 0) + const name_str = if ((try description.className(globalThis)).toSlice(allocator).length() == 0) description.getName(globalThis).toSlice(allocator).slice() else - description.className(globalThis).toSlice(allocator).slice(); + (try description.className(globalThis)).toSlice(allocator).slice(); break :brk try allocator.dupe(u8, name_str); } if (description.isFunction()) { diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index e58ee6ab40ab97..b0c588b25e9e64 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -761,7 +761,9 @@ pub const JestPrettyFormat = struct { if (this.formatter.indent == 0) this.writer.writeAll("\n") catch {}; var classname = ZigString.Empty; - value.getClassName(globalThis, &classname); + value.getClassName(globalThis, &classname) catch |e| { + _ = globalThis.takeException(e); + }; if (!classname.isEmpty() and !classname.eqlComptime("Object")) { this.writer.print("{} ", .{classname}) catch {}; } @@ -1100,7 +1102,7 @@ pub const JestPrettyFormat = struct { }, .Error => { var classname = ZigString.Empty; - value.getClassName(this.globalThis, &classname); + try value.getClassName(this.globalThis, &classname); var message_string = bun.String.empty; defer message_string.deref(); @@ -1117,7 +1119,7 @@ pub const JestPrettyFormat = struct { }, .Class => { var printable = ZigString.init(&name_buf); - value.getClassName(this.globalThis, &printable); + try value.getClassName(this.globalThis, &printable); this.addForNewLine(printable.len); if (printable.len == 0) { @@ -1300,7 +1302,7 @@ pub const JestPrettyFormat = struct { } else if (value.as(bun.api.ResolveMessage)) |resolve_log| { resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {}; return; - } else if (printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) { + } else if (try printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) { return; } else if (jsType != .DOMWrapper) { if (value.isCallable()) { @@ -1768,7 +1770,7 @@ pub const JestPrettyFormat = struct { if (iter.i == 0) { var object_name = ZigString.Empty; - value.getClassName(this.globalThis, &object_name); + try value.getClassName(this.globalThis, &object_name); if (!object_name.eqlComptime("Object")) { writer.print("{s} {{}}", .{object_name}); @@ -1805,7 +1807,7 @@ pub const JestPrettyFormat = struct { if (jsType == .Uint8Array) { var buffer_name = ZigString.Empty; - value.getClassName(this.globalThis, &buffer_name); + try value.getClassName(this.globalThis, &buffer_name); if (strings.eqlComptime(buffer_name.slice(), "Buffer")) { // special formatting for 'Buffer' snapshots only if (slice.len == 0 and this.indent == 0) writer.writeAll("\n"); @@ -2048,7 +2050,7 @@ pub const JestPrettyFormat = struct { name_buf: [512]u8, value: JSValue, comptime enable_ansi_colors: bool, - ) bool { + ) !bool { _ = Format; if (value.as(expect.ExpectAnything)) |matcher| { @@ -2073,7 +2075,7 @@ pub const JestPrettyFormat = struct { } var class_name = ZigString.init(&name_buf); - constructor_value.getClassName(this.globalThis, &class_name); + try constructor_value.getClassName(this.globalThis, &class_name); this.addForNewLine(class_name.len); writer.print(comptime Output.prettyFmt("{}", enable_ansi_colors), .{class_name}); this.addForNewLine(1); diff --git a/src/cpp.zig b/src/cpp.zig new file mode 100644 index 00000000000000..b0e35e50dafe92 --- /dev/null +++ b/src/cpp.zig @@ -0,0 +1,30 @@ +const bun = @import("bun"); +const JSC = bun.JSC; +const HTTPServerAgent = bun.jsc.Debugger.HTTPServerAgent; + +const raw = struct { + /// Source: bun.js/bindings/BunString.cpp:401:32 + extern fn BunString__toJSON(globalObject: *JSC.JSGlobalObject, bunString: *bun.String) JSC.JSValue; + + /// Source: bun.js/bindings/bindings.cpp:5177:17 + extern fn JSC__JSValue__toZigException(jsException: JSC.JSValue, global: *JSC.JSGlobalObject, exception: *bun.JSC.ZigException) void; +}; + +pub const bindings = struct { + pub inline fn BunString__toJSON(globalObject: *JSC.JSGlobalObject, bunString: *bun.String) bun.JSError!JSC.JSValue { + return bun.JSC.fromJSHostCall(raw.BunString__toJSON, @src(), .{ globalObject, bunString }); + } + + /// Source: bun.js/bindings/InspectorHTTPServerAgent.cpp:191:6 + pub extern fn Bun__HTTPServerAgent__notifyServerStarted(agent: *HTTPServerAgent.InspectorHTTPServerAgent, serverId: HTTPServerAgent.ServerId, hotReloadId: HTTPServerAgent.HotReloadId, address: *const bun.String, startTime: f64, serverInstance: *anyopaque) void; + + /// Source: bun.js/bindings/InspectorHTTPServerAgent.cpp:198:6 + pub extern fn Bun__HTTPServerAgent__notifyServerStopped(agent: *HTTPServerAgent.InspectorHTTPServerAgent, serverId: HTTPServerAgent.ServerId, timestamp: f64) void; + + /// Source: bun.js/bindings/InspectorHTTPServerAgent.cpp:225:6 + pub extern fn Bun__HTTPServerAgent__notifyServerRoutesUpdated(agent: *HTTPServerAgent.InspectorHTTPServerAgent, serverId: HTTPServerAgent.ServerId, hotReloadId: HTTPServerAgent.HotReloadId, routes_ptr: *HTTPServerAgent.Route, routes_len: usize) void; + + pub inline fn JSC__JSValue__toZigException(jsException: JSC.JSValue, global: *JSC.JSGlobalObject, exception: *bun.JSC.ZigException) bun.JSError!void { + return bun.JSC.fromJSHostCallGeneric(raw.JSC__JSValue__toZigException, @src(), .{ jsException, global, exception }); + } +}; diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 53b5cf60ca4572..49f12b36f20fab 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -4854,6 +4854,14 @@ describe("expect()", () => { expect(e.message).toContain('"ball"'); } }); + + test("does not crash with non-cell error", () => { + expect(() => { + expect(() => { + throw 25; + }).toThrow(Error); + }).toThrow(); + }); }); function tmpFile(exists) { diff --git a/test/js/deno/event/event.test.ts b/test/js/deno/event/event.test.ts index 633ba51dabd0cb..58ec7ca719d420 100644 --- a/test/js/deno/event/event.test.ts +++ b/test/js/deno/event/event.test.ts @@ -73,10 +73,11 @@ test(function eventInitializedWithNonStringType() { assertEquals(event.cancelable, false); }); test(function eventIsTrusted() { - const desc1 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + // Node breaks web spec and puts the isTrusted getter on the prototype + const desc1 = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(new Event("x")), "isTrusted"); assert(desc1); assertEquals(typeof desc1.get, "function"); - const desc2 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + const desc2 = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(new Event("x")), "isTrusted"); assert(desc2); assertEquals(typeof desc2!.get, "function"); assertEquals(desc1!.get, desc2!.get); diff --git a/test/js/node/abortcontroller.test.ts b/test/js/node/abortcontroller.test.ts new file mode 100644 index 00000000000000..3cecfa3ba5d796 --- /dev/null +++ b/test/js/node/abortcontroller.test.ts @@ -0,0 +1,354 @@ +import { describe, expect, test } from "bun:test"; + +describe("AbortController", () => { + test("basic functionality", () => { + const controller = new AbortController(); + expect(controller).toBeInstanceOf(AbortController); + expect(controller.signal).toBeInstanceOf(AbortSignal); + expect(controller.signal.aborted).toBe(false); + + controller.abort(); + expect(controller.signal.aborted).toBe(true); + expect(controller.signal.reason).toBeInstanceOf(DOMException); + expect(controller.signal.reason.name).toBe("AbortError"); + }); + + test("abort with custom reason", () => { + const controller = new AbortController(); + const customReason = new Error("Custom abort reason"); + controller.abort(customReason); + + expect(controller.signal.aborted).toBe(true); + expect(controller.signal.reason).toBe(customReason); + }); + + test("event listener for abort event", () => { + return new Promise(resolve => { + const controller = new AbortController(); + let eventFired = false; + + controller.signal.addEventListener("abort", event => { + eventFired = true; + expect(event.type).toBe("abort"); + expect(controller.signal.aborted).toBe(true); + resolve(); + }); + + controller.abort(); + expect(eventFired).toBe(true); + }); + }); + + test("onabort property", () => { + return new Promise(resolve => { + const controller = new AbortController(); + + controller.signal.onabort = event => { + expect(event.type).toBe("abort"); + expect(controller.signal.aborted).toBe(true); + resolve(); + }; + + controller.abort(); + }); + }); + + test("throwIfAborted method", () => { + const controller = new AbortController(); + + // Should not throw when not aborted + expect(() => controller.signal.throwIfAborted()).not.toThrow(); + + // Should throw after abort + controller.abort(); + expect(() => controller.signal.throwIfAborted()).toThrow(DOMException); + + try { + controller.signal.throwIfAborted(); + } catch (error: unknown) { + expect(error instanceof DOMException).toBe(true); + if (error instanceof DOMException) { + expect(error.name).toBe("AbortError"); + } + } + }); + + test("throwIfAborted with custom reason", () => { + const controller = new AbortController(); + const customReason = new Error("Custom abort reason"); + + controller.abort(customReason); + + try { + controller.signal.throwIfAborted(); + } catch (error: unknown) { + expect(error).toBe(customReason); + } + }); +}); + +describe("AbortSignal static methods", () => { + test("AbortSignal.abort()", () => { + const signal = AbortSignal.abort(); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(true); + expect(signal.reason).toBeInstanceOf(DOMException); + }); + + test("AbortSignal.abort() with custom reason", () => { + const customReason = new Error("Custom static abort reason"); + const signal = AbortSignal.abort(customReason); + + expect(signal.aborted).toBe(true); + expect(signal.reason).toBe(customReason); + }); + + test("AbortSignal.any()", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + + const anySignal = AbortSignal.any([controller1.signal, controller2.signal]); + expect(anySignal).toBeInstanceOf(AbortSignal); + expect(anySignal.aborted).toBe(false); + + return new Promise(resolve => { + anySignal.addEventListener("abort", () => { + expect(anySignal.aborted).toBe(true); + expect(anySignal.reason).toBeInstanceOf(DOMException); + resolve(); + }); + + // Abort one of the controllers + controller1.abort(); + }); + }); + + test("AbortSignal.any() with already aborted signal", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + + controller1.abort(); + + const anySignal = AbortSignal.any([controller1.signal, controller2.signal]); + expect(anySignal.aborted).toBe(true); + }); +}); + +describe("AbortController", () => { + test("abort with custom reason", () => { + const controller = new AbortController(); + const reason = new Error("Custom abort reason"); + controller.abort(reason); + + expect(controller.signal.aborted).toBe(true); + expect(controller.signal.reason).toBe(reason); + }); + + test("abort event fires once", () => { + const controller = new AbortController(); + let callCount = 0; + + controller.signal.addEventListener("abort", () => { + callCount++; + }); + + controller.abort(); + controller.abort(); // Second abort should not trigger listener again + + expect(callCount).toBe(1); + }); + + test("onabort handler", () => { + const controller = new AbortController(); + let handlerCalled = false; + let eventType = ""; + + controller.signal.onabort = event => { + handlerCalled = true; + eventType = event.type; + }; + + controller.abort(); + + expect(handlerCalled).toBe(true); + expect(eventType).toBe("abort"); + }); + + test("throwIfAborted when not aborted", () => { + const controller = new AbortController(); + expect(() => controller.signal.throwIfAborted()).not.toThrow(); + }); + + test("throwIfAborted when aborted", () => { + const controller = new AbortController(); + controller.abort(); + + expect(() => controller.signal.throwIfAborted()).toThrow(DOMException); + expect(() => controller.signal.throwIfAborted()).toThrow("The operation was aborted"); + }); + + test("throwIfAborted with custom reason", () => { + const controller = new AbortController(); + const reason = new TypeError("Custom abort type"); + controller.abort(reason); + + expect(() => controller.signal.throwIfAborted()).toThrow(TypeError); + expect(() => controller.signal.throwIfAborted()).toThrow("Custom abort type"); + }); +}); + +describe("AbortSignal.abort", () => { + test("creates pre-aborted signal", () => { + const signal = AbortSignal.abort(); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(true); + expect(signal.reason).toBeInstanceOf(DOMException); + expect(signal.reason.name).toBe("AbortError"); + }); + + test("creates signal with custom reason", () => { + const reason = { message: "Custom object reason" }; + const signal = AbortSignal.abort(reason); + + expect(signal.aborted).toBe(true); + expect(signal.reason).toBe(reason); + }); + + test("abort event does not fire on pre-aborted signal", () => { + const signal = AbortSignal.abort(); + let eventFired = false; + + signal.addEventListener("abort", () => { + eventFired = true; + }); + + // The event should not fire because the signal is already aborted + expect(eventFired).toBe(false); + }); +}); + +describe("AbortSignal.timeout", () => { + test("creates signal that aborts after timeout", async () => { + const signal = AbortSignal.timeout(50); + expect(signal.aborted).toBe(false); + + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(signal.aborted).toBe(true); + expect(signal.reason).toBeInstanceOf(DOMException); + expect(signal.reason.name).toBe("TimeoutError"); + }); + + test("fires abort event after timeout", async () => { + const signal = AbortSignal.timeout(50); + let eventFired = false; + + signal.addEventListener("abort", () => { + eventFired = true; + }); + + expect(eventFired).toBe(false); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(eventFired).toBe(true); + }); + + test("immediate timeout (0ms)", async () => { + const signal = AbortSignal.timeout(0); + // Even with 0ms, the abort happens on the next tick + expect(signal.aborted).toBe(false); + + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(signal.aborted).toBe(true); + }); + + test("throws on invalid timeout values", () => { + expect(() => AbortSignal.timeout(-1)).toThrow(TypeError); + expect(() => AbortSignal.timeout(NaN)).toThrow(TypeError); + expect(() => AbortSignal.timeout(Infinity)).toThrow(TypeError); + }); +}); + +describe("AbortSignal.any", () => { + test("aborts when any signal aborts", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + + const anySignal = AbortSignal.any([controller1.signal, controller2.signal]); + expect(anySignal.aborted).toBe(false); + + controller1.abort(); + expect(anySignal.aborted).toBe(true); + expect(anySignal.reason).toBe(controller1.signal.reason); + }); + + test("aborts with reason from first aborted signal", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + + const reason1 = new Error("First controller aborted"); + const reason2 = new Error("Second controller aborted"); + + const anySignal = AbortSignal.any([controller1.signal, controller2.signal]); + + controller2.abort(reason2); + controller1.abort(reason1); // This should have no effect since controller2 already aborted + + expect(anySignal.aborted).toBe(true); + expect(anySignal.reason).toBe(reason2); + }); + + test("aborts immediately if any signal is already aborted", () => { + const abortedSignal = AbortSignal.abort("Already aborted"); + const controller = new AbortController(); + + const anySignal = AbortSignal.any([abortedSignal, controller.signal]); + + expect(anySignal.aborted).toBe(true); + expect(anySignal.reason).toBe("Already aborted"); + }); + + test("works with empty array", () => { + const anySignal = AbortSignal.any([]); + expect(anySignal.aborted).toBe(false); + }); + + test("fires abort event", () => { + const controller1 = new AbortController(); + const controller2 = new AbortController(); + + const anySignal = AbortSignal.any([controller1.signal, controller2.signal]); + let eventFired = false; + + anySignal.addEventListener("abort", () => { + eventFired = true; + }); + + controller2.abort(); + expect(eventFired).toBe(true); + }); +}); + +describe("AbortSignal integration", () => { + test("Promise with AbortSignal", async () => { + expect.assertions(1); + + const controller = new AbortController(); + + const promise = new Promise((resolve, reject) => { + controller.signal.addEventListener("abort", () => { + reject(controller.signal.reason); + }); + + // Simulate a long operation + setTimeout(resolve, 1000); + }); + + // Abort before the timeout completes + controller.abort(); + + try { + await promise; + } catch (error) { + expect(error).toBeInstanceOf(DOMException); + } + }); +}); diff --git a/test/js/node/test/parallel/test-abortcontroller.js b/test/js/node/test/parallel/test-abortcontroller.js new file mode 100644 index 00000000000000..ab99cdc75c4ad4 --- /dev/null +++ b/test/js/node/test/parallel/test-abortcontroller.js @@ -0,0 +1,285 @@ +// Flags: --expose-gc +'use strict'; + +require('../common'); +const { inspect } = require('util'); + +const { + ok, + notStrictEqual, + strictEqual, + throws, +} = require('assert'); + +const { + test, + mock, +} = require('node:test'); + +const { setTimeout: sleep } = require('timers/promises'); + +// All of the the tests in this file depend on public-facing Node.js APIs. +// For tests that depend on Node.js internal APIs, please add them to +// test-abortcontroller-internal.js instead. + +test('Abort is fired with the correct event type on AbortControllers', () => { + // Tests that abort is fired with the correct event type on AbortControllers + const ac = new AbortController(); + ok(ac.signal); + + let calls = 0; + const fn = (event) => { + ok(event); + strictEqual(event.type, 'abort'); + calls++; + }; + + ac.signal.onabort = fn; + ac.signal.addEventListener('abort', fn); + + ac.abort(); + ac.abort(); + ok(ac.signal.aborted); + + strictEqual(calls, 2); +}); + +test('Abort events are trusted', () => { + // Tests that abort events are trusted + const ac = new AbortController(); + + let calls = 0; + const fn = (event) => { + ok(event.isTrusted); + calls++; + }; + + ac.signal.onabort = fn; + ac.abort(); + strictEqual(calls, 1); +}); + +test('Abort events have the same isTrusted reference', () => { + // Tests that abort events have the same `isTrusted` reference + const first = new AbortController(); + const second = new AbortController(); + let ev1, ev2; + const ev3 = new Event('abort'); + + first.signal.addEventListener('abort', (event) => { + ev1 = event; + }); + second.signal.addEventListener('abort', (event) => { + ev2 = event; + }); + first.abort(); + second.abort(); + const firstTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev1), 'isTrusted').get; + const secondTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev2), 'isTrusted').get; + const untrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev3), 'isTrusted').get; + strictEqual(firstTrusted, secondTrusted); + strictEqual(untrusted, firstTrusted); +}); + +test('AbortSignal is impossible to construct manually', () => { + // Tests that AbortSignal is impossible to construct manually + const ac = new AbortController(); + throws(() => new ac.signal.constructor(), { + code: 'ERR_ILLEGAL_CONSTRUCTOR', + }); +}); + +test('Symbol.toStringTag is correct', () => { + // Symbol.toStringTag + const toString = (o) => Object.prototype.toString.call(o); + const ac = new AbortController(); + strictEqual(toString(ac), '[object AbortController]'); + strictEqual(toString(ac.signal), '[object AbortSignal]'); +}); + +test('AbortSignal.abort() creates an already aborted signal', () => { + const signal = AbortSignal.abort(); + ok(signal.aborted); +}); + +test('AbortController properties and methods valiate the receiver', () => { + const acSignalGet = Object.getOwnPropertyDescriptor( + AbortController.prototype, + 'signal' + ).get; + const acAbort = AbortController.prototype.abort; + + const goodController = new AbortController(); + ok(acSignalGet.call(goodController)); + acAbort.call(goodController); + + const badAbortControllers = [ + null, + undefined, + 0, + NaN, + true, + 'AbortController', + { __proto__: AbortController.prototype }, + ]; + for (const badController of badAbortControllers) { + throws( + () => acSignalGet.call(badController), + { name: 'TypeError' } + ); + throws( + () => acAbort.call(badController), + { name: 'TypeError' } + ); + } +}); + +test('AbortSignal properties validate the receiver', () => { + const signalAbortedGet = Object.getOwnPropertyDescriptor( + AbortSignal.prototype, + 'aborted' + ).get; + + const goodSignal = new AbortController().signal; + strictEqual(signalAbortedGet.call(goodSignal), false); + + const badAbortSignals = [ + null, + undefined, + 0, + NaN, + true, + 'AbortSignal', + { __proto__: AbortSignal.prototype }, + ]; + for (const badSignal of badAbortSignals) { + throws( + () => signalAbortedGet.call(badSignal), + { name: 'TypeError' } + ); + } +}); + +test('AbortController inspection depth 1 or null works', () => { + const ac = new AbortController(); + strictEqual(inspect(ac, { depth: 1 }), + 'AbortController { signal: [AbortSignal] }'); + strictEqual(inspect(ac, { depth: null }), + 'AbortController { signal: AbortSignal { aborted: false } }'); +}); + +test('AbortSignal reason is set correctly', () => { + // Test AbortSignal.reason + const ac = new AbortController(); + ac.abort('reason'); + strictEqual(ac.signal.reason, 'reason'); +}); + +test('AbortSignal reasonable is set correctly with AbortSignal.abort()', () => { + // Test AbortSignal.reason + const signal = AbortSignal.abort('reason'); + strictEqual(signal.reason, 'reason'); +}); + +test('AbortSignal.timeout() works as expected', async () => { + // Test AbortSignal timeout + const signal = AbortSignal.timeout(10); + ok(!signal.aborted); + + const { promise, resolve } = Promise.withResolvers(); + + const fn = () => { + ok(signal.aborted); + strictEqual(signal.reason.name, 'TimeoutError'); + strictEqual(signal.reason.code, 23); + resolve(); + }; + + setTimeout(fn, 20); + await promise; +}); + +test('AbortSignal.timeout() does not prevent the signal from being collected', async () => { + // Test AbortSignal timeout doesn't prevent the signal + // from being garbage collected. + let ref; + { + ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000)); + } + + await sleep(10); + if(typeof Bun !== "undefined") { + Bun.gc(true); + }else{ + globalThis.gc(); + } + strictEqual(ref.deref(), undefined); +}); + +test('AbortSignal with a timeout is not collected while there is an active listener', async () => { + // Test that an AbortSignal with a timeout is not gc'd while + // there is an active listener on it. + let ref; + function handler() {} + { + ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000)); + ref.deref().addEventListener('abort', handler); + } + + await sleep(10); + globalThis.gc(); + notStrictEqual(ref.deref(), undefined); + ok(ref.deref() instanceof AbortSignal); + + ref.deref().removeEventListener('abort', handler); + + await sleep(10); + globalThis.gc(); + if (typeof Bun !== "undefined") Bun.gc(true); // only force gc will collect the signal in bun + strictEqual(ref.deref(), undefined); +}); + +test('Setting a long timeout should not keep the process open', () => { + AbortSignal.timeout(1_200_000); +}); + +test('AbortSignal.reason should default', () => { + // Test AbortSignal.reason default + const signal = AbortSignal.abort(); + ok(signal.reason instanceof DOMException); + strictEqual(signal.reason.code, 20); + + const ac = new AbortController(); + ac.abort(); + ok(ac.signal.reason instanceof DOMException); + strictEqual(ac.signal.reason.code, 20); +}); + +test('abortSignal.throwIfAborted() works as expected', () => { + // Test abortSignal.throwIfAborted() + throws(() => AbortSignal.abort().throwIfAborted(), { + code: 20, + name: 'AbortError', + }); + + // Does not throw because it's not aborted. + const ac = new AbortController(); + ac.signal.throwIfAborted(); +}); + +test('abortSignal.throwIfAobrted() works as expected (2)', () => { + const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted'); + const actualReason = new Error(); + Reflect.defineProperty(AbortSignal.prototype, 'aborted', { value: false }); + throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason); + Reflect.defineProperty(AbortSignal.prototype, 'aborted', originalDesc); +}); + +test('abortSignal.throwIfAobrted() works as expected (3)', () => { + const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'reason'); + const actualReason = new Error(); + const fakeExcuse = new Error(); + Reflect.defineProperty(AbortSignal.prototype, 'reason', { value: fakeExcuse }); + throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason); + Reflect.defineProperty(AbortSignal.prototype, 'reason', originalDesc); +}); diff --git a/test/js/node/test/parallel/test-eventtarget.js b/test/js/node/test/parallel/test-eventtarget.js index 6dac98e29ce544..c646c3c71269b6 100644 --- a/test/js/node/test/parallel/test-eventtarget.js +++ b/test/js/node/test/parallel/test-eventtarget.js @@ -652,7 +652,7 @@ if (typeof Bun === "undefined") { // Node internal if (typeof Bun === "undefined") { strictEqual(evConstructorName, 'Event'); } else { - strictEqual(evConstructorName, '[Event]'); + strictEqual(evConstructorName, 'Event {}'); } const inspectResult = inspect(ev, { diff --git a/test/js/web/abort/abort-signal-reason.test.js b/test/js/web/abort/abort-signal-reason.test.js new file mode 100644 index 00000000000000..39a7b8e7c4ebcc --- /dev/null +++ b/test/js/web/abort/abort-signal-reason.test.js @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; + +describe("AbortSignal reason", () => { + // https://bugs.webkit.org/show_bug.cgi?id=293319 + test("reason is preserved after GC", () => { + const controller = new AbortController(); + controller.signal; + controller.abort(new Error("one two three")); // error must be defined inline so it doesn't get kept alive + Bun.gc(true); + + let error; + try { + controller.signal.throwIfAborted(); + } catch (e) { + error = e; + } + + expect(error).toBe(controller.signal.reason); + expect(error.message).toBe("one two three"); + }); +});