diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 1c760357186a..01e9dc725b06 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -86,6 +86,7 @@ inline To tryJSDynamicCast(JSC::WriteBarrier& from) } JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall); +JSC_DECLARE_HOST_FUNCTION(jsMockFunctionConstruct); JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl); JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock); JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetter_mockGetLastCall); @@ -462,7 +463,7 @@ class JSMockFunction : public JSC::InternalFunction { } JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind) - : Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall) + : Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct) { initMock(); } @@ -981,6 +982,21 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObje return JSValue::encode(jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe); + RETURN_IF_EXCEPTION(scope, {}); + JSValue result = JSValue::decode(encodedResult); + if (result.isObject()) + return encodedResult; + JSObject* newTarget = asObject(callframe->newTarget()); + Structure* structure = JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, lexicalGlobalObject->objectStructureForObjectConstructor()); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(JSC::constructEmptyObject(vm, structure)); +} + void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { Base::finishCreation(vm); diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp index 8db7b462b33a..60782fc937e5 100644 --- a/src/bun.js/bindings/webcore/Worker.cpp +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -133,12 +133,21 @@ extern "C" void WebWorker__setRef( void Worker::setKeepAlive(bool keepAlive) { - WebWorker__setRef(impl_, keepAlive); + Locker locker { m_implLock }; + if (impl_) + WebWorker__setRef(impl_, keepAlive); +} + +void Worker::clearZigImpl() +{ + Locker locker { m_implLock }; + impl_ = nullptr; } bool Worker::updatePtr() { if (!WebWorker__updatePtr(impl_, this)) { + clearZigImpl(); m_onlineClosingFlags = ClosingFlag; m_terminationFlags.fetch_or(TerminatedFlag); return false; @@ -263,7 +272,9 @@ void Worker::terminate() { // m_contextProxy.terminateWorkerGlobalScope(); m_terminationFlags.fetch_or(TerminateRequestedFlag); - WebWorker__notifyNeedTermination(impl_); + Locker locker { m_implLock }; + if (impl_) + WebWorker__notifyNeedTermination(impl_); } // const char* Worker::activeDOMObjectName() const @@ -468,6 +479,8 @@ void Worker::forEachWorker(const FunctiondispatchExit(exitCode); + // The Zig WebWorker is about to be freed; prevent terminate()/ref()/unref() from touching it. + worker->clearZigImpl(); // no longer referenced by Zig worker->deref(); diff --git a/src/bun.js/bindings/webcore/Worker.h b/src/bun.js/bindings/webcore/Worker.h index bbc73053ddfe..906d0d000bc4 100644 --- a/src/bun.js/bindings/webcore/Worker.h +++ b/src/bun.js/bindings/webcore/Worker.h @@ -76,6 +76,7 @@ class Worker final : public ThreadSafeRefCounted, public EventTargetWith void dispatchEvent(Event&); void dispatchCloseEvent(Event&); void setKeepAlive(bool); + void clearZigImpl(); void postTaskToWorkerGlobalScope(Function&&); @@ -119,6 +120,7 @@ class Worker final : public ThreadSafeRefCounted, public EventTargetWith // Tracks TerminateRequestedFlag and TerminatedFlag std::atomic m_terminationFlags { 0 }; const ScriptExecutionContextIdentifier m_clientIdentifier; + Lock m_implLock; void* impl_ { nullptr }; }; diff --git a/test/js/bun/test/mock-construct.test.ts b/test/js/bun/test/mock-construct.test.ts new file mode 100644 index 000000000000..3d55a95c20e3 --- /dev/null +++ b/test/js/bun/test/mock-construct.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, mock, test } from "bun:test"; + +describe("mock() used as a constructor", () => { + test("Reflect.construct with no implementation returns an object", () => { + const m = mock(); + const r = Reflect.construct(m, []); + expect(typeof r).toBe("object"); + expect(r).not.toBeNull(); + }); + + test("Reflect.construct with a different newTarget returns an object", () => { + const m = mock(); + const r = Reflect.construct(m, [], function () {}); + expect(typeof r).toBe("object"); + expect(r).not.toBeNull(); + }); + + test("Reflect.construct with implementation returning a primitive returns an object", () => { + const m = mock(() => 42); + const r = Reflect.construct(m, []); + expect(typeof r).toBe("object"); + expect(r).not.toBeNull(); + }); + + test("Reflect.construct with mockReturnValue(primitive) returns an object", () => { + const m = mock().mockReturnValue(7); + const r = Reflect.construct(m, []); + expect(typeof r).toBe("object"); + expect(r).not.toBeNull(); + }); + + test("Reflect.construct preserves object return values", () => { + const obj = { hello: "world" }; + const m = mock(() => obj); + const r = Reflect.construct(m, []); + expect(r).toBe(obj); + }); + + test("Reflect.construct with a custom newTarget honors its prototype", () => { + class Foo {} + const m = mock(); + const r = Reflect.construct(m, [], Foo); + expect(r).toBeInstanceOf(Foo); + }); + + test("calling without new still returns the implementation's value", () => { + const m1 = mock(); + expect(m1()).toBeUndefined(); + const m2 = mock(() => 42); + expect(m2()).toBe(42); + const m3 = mock().mockReturnValue("x"); + expect(m3()).toBe("x"); + }); +}); diff --git a/test/js/web/workers/worker-terminate-after-exit.test.ts b/test/js/web/workers/worker-terminate-after-exit.test.ts new file mode 100644 index 000000000000..4280e2b41933 --- /dev/null +++ b/test/js/web/workers/worker-terminate-after-exit.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; + +describe("Worker", () => { + test("terminate()/ref()/unref() after the worker has exited does not crash", async () => { + const url = URL.createObjectURL(new Blob(["/* exits immediately */"], { type: "application/javascript" })); + try { + const workers: Worker[] = []; + const exited: Promise[] = []; + for (let i = 0; i < 4; i++) { + const { promise, resolve } = Promise.withResolvers(); + const w = new Worker(url); + w.addEventListener("close", () => resolve(), { once: true }); + workers.push(w); + exited.push(promise); + } + + await Promise.all(exited); + + Bun.gc(true); + + for (const w of workers) { + w.terminate(); + w.ref(); + w.unref(); + w.terminate(); + } + expect(workers.length).toBe(4); + } finally { + URL.revokeObjectURL(url); + } + }); +});