diff options
author | 2023-07-16 21:15:24 -0700 | |
---|---|---|
committer | 2023-07-16 21:15:24 -0700 | |
commit | 209dc981c0ef52de5c80eab5d24ec8a8eba765e8 (patch) | |
tree | c3d203d03d109161bcaf25558c05fda49c5e864e | |
parent | 7fc392b182fa45d1fa33e654c2b4d1f1022a1ac3 (diff) | |
download | bun-209dc981c0ef52de5c80eab5d24ec8a8eba765e8.tar.gz bun-209dc981c0ef52de5c80eab5d24ec8a8eba765e8.tar.zst bun-209dc981c0ef52de5c80eab5d24ec8a8eba765e8.zip |
Implement Workers (#3645)
* copy files
* format
* options
* Introduce `Worker`, `onmessage`, `onerror`, and `postMessage` globals
* Stub `Worker.prototype.ref` & `Worker.prototype.unref`
* Update web_worker.zig
* Worker works
* Add "mini" mode
* add wakeup
* Partially fix the keep-alive issue
* clean up refer behavior
* Implement `serialize` & `deserialize` in `bun:jsc` & add polyfill for `node:v8`
* Types & docs
* Update globals.d.ts
* Add mutex
* Fixes
---------
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
46 files changed, 2796 insertions, 75 deletions
diff --git a/docs/api/worker.md b/docs/api/worker.md new file mode 100644 index 000000000..c5a3da1a9 --- /dev/null +++ b/docs/api/worker.md @@ -0,0 +1,162 @@ +[`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) lets you start and communicate with a new JavaScript instance running on a separate thread while sharing I/O resources with the main thread. You can use TypeScript, CommonJS, ESM, JSX, etc in your workers. `Worker` support was added in Bun v0.6.15. + +Bun implements a minimal version of the [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) with extensions that make it work better for server-side use cases. + +## Usage + +Like in browsers, [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) is a global. Use it to create a new worker thread. + +Main thread: + +```js +const worker = new Worker(new URL("worker.ts", import.meta.url).href); +worker.postMessage("hello"); +worker.onmessage = event => { + console.log(event.data); +}; +``` + +Worker thread: + +{% codetabs %} + +```ts#worker.ts +self.onmessage = (event: MessageEvent) => { + console.log(event.data); + postMessage("world"); +}; +``` + +{% /codetabs %} + +### Sending & receiving messages with `postMessage` + +To send messages, use [`worker.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) and [`self.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This leverages the [HTML Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). + +```js +// On the worker thread, `postMessage` is automatically "routed" to the parent thread. +postMessage({ hello: "world" }); + +// On the main thread +worker.postMessage({ hello: "world" }); +``` + +To receive messages, use the [`message` event handler](https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event) on the worker and main thread. + +```js +// Worker thread: +self.addEventListener("message", = event => { + console.log(event.data); +}); +// or use the setter: +// self.onmessage = fn + +// if on the main thread +worker.addEventListener("message", = event => { + console.log(event.data); +}); +// or use the setter: +// worker.onmessage = fn +``` + +### Terminating a worker + +A `Worker` instance terminate automatically when Bun's process exits. To terminate a `Worker` sooner, call `worker.terminate()`. + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href); + +// ...some time later +worker.terminate(); +``` + +### Managing lifetime with `worker.ref` and `worker.unref` + +By default, a `Worker` will **not** keep the process alive. To keep the process alive until the `Worker` terminates, call `worker.ref()`. + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href); +worker.ref(); +``` + +You can also pass an `options` object to `Worker`: + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href, { + bun: { + ref: true, + }, +}); +``` + +To stop keeping the process alive, call `worker.unref()`. + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href); +worker.ref(); +// ...later on +worker.unref(); +``` + +Note: `worker.ref()` and `worker.unref()` do not exist in browsers. + +### Memory Usage + +JavaScript instances sometimes use a lot of memory. + +Bun's `Worker` supports a `smol` mode that reduces memory usage, at a cost of performance. To enable `smol` mode, pass `smol: true` to the `options` object in the `Worker` constructor. + +```js +const worker = new Worker("./i-am-smol.ts", { + bun: { + smol: true, + }, +}); +``` + +#### What does `smol` mode actually do? + +It sets ` JSC::HeapSize` to be `Small` instead of the default `Large` + +### Modules work + +Like the rest of Bun, `Worker` in Bun support CommonJS, ES Modules, TypeScript, JSX, TSX and more out of the box. No extra build steps are necessary. You can use `import` and `export` in your worker code. This is different than browsers, where `"type": "module"` is necessary to use ES Modules. + +To simplify error handling, the initial script to load is resolved at the time `new Worker(url)` is called. + +```js +const worker = new Worker("/not-found.js"); +// throws an error immediately +``` + +The specifier passed to `Worker` is resolved relative to the project root (like typing `bun ./path/to/file.js`). + +### `"open"` event + +The `"open"` event is emitted when a worker is created and ready to receive messages. + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href); +worker.addEventListener("open", () => { + console.log("worker is ready"); +}); +``` + +This event does not exist in browsers. + +### `"close"` event + +The `"close"` event is emitted when a worker has been terminated. It can take some time for the worker to actually terminate, so this event is emitted when the worker has been marked as terminated. + +```ts +const worker = new Worker(new URL("worker.ts", import.meta.url).href); +worker.addEventListener("close", () => { + console.log("worker is ready"); +}); +``` + +This event does not exist in browsers. + +### `process.exit()` inside a worker + +Calling `process.exit()` in a Worker terminates the worker, but does not terminate the main process. Like in Node.js, `process.on('beforeExit', callback)` and `process.on('exit', callback)` are emitted on the worker thread (and not on the main thread). diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 2e45fd945..db4844b7e 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -231,7 +231,7 @@ This page is updated regularly to reflect compatibility status of the latest ver - {% anchor id="node_v8" %} [`node:v8`](https://nodejs.org/api/v8.html) {% /anchor %} - 🔴 -- Not implemented or planned. For profiling, use [`bun:jsc`](/docs/project/benchmarking#bunjsc) instead. +- `serialize` and `deserialize` use JavaScriptCore's wire format instead of V8's. Otherwise, not implemented. For profiling, use [`bun:jsc`](/docs/project/benchmarking#bunjsc) instead. --- diff --git a/docs/runtime/web-apis.md b/docs/runtime/web-apis.md index e1d95fa9f..46728bb62 100644 --- a/docs/runtime/web-apis.md +++ b/docs/runtime/web-apis.md @@ -16,6 +16,13 @@ The following Web APIs are partially or completely supported. --- +--- + +- Web Workers +- [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) [`self.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage) [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) + +--- + - Streams - [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) [`TransformStream`](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream) [`ByteLengthQueuingStrategy`](https://developer.mozilla.org/en-US/docs/Web/API/ByteLengthQueuingStrategy) [`CountQueuingStrategy`](https://developer.mozilla.org/en-US/docs/Web/API/CountQueuingStrategy) and associated classes diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 62bc37a82..e4fefcc11 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -2283,6 +2283,8 @@ interface FetchEvent extends Event { interface EventMap { fetch: FetchEvent; + message: MessageEvent; + messageerror: MessageEvent; // exit: Event; } @@ -3498,6 +3500,78 @@ interface EventSourceEventMap { open: Event; } +interface Worker extends EventTarget { + onerror: ((this: Worker, ev: ErrorEvent) => any) | null; + onmessage: ((this: Worker, ev: MessageEvent) => any) | null; + onmessageerror: ((this: Worker, ev: MessageEvent) => any) | null; + + addEventListener<K extends keyof WorkerEventMap>( + type: K, + listener: (this: Worker, ev: WorkerEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + + removeEventListener<K extends keyof WorkerEventMap>( + type: K, + listener: (this: Worker, ev: WorkerEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + + terminate(): void; + + postMessage(message: any, transfer?: Transferable[]): void; + + /** + * Keep the process alive until the worker is terminated or `unref`'d + */ + ref(): void; + /** + * Undo a previous `ref()` + */ + unref(): void; +} + +/** + * Post a message to the parent thread. + * + * Only useful in a worker thread; calling this from the main thread does nothing. + */ +declare function postMessage(message: any, transfer?: Transferable[]): void; + +declare var Worker: { + prototype: Worker; + new (stringUrl: string | URL, options?: WorkerOptions): Worker; +}; + +interface WorkerOptions { + name?: string; + bun?: { + /** + * Use less memory, but make the worker slower. + * + * Internally, this sets the heap size configuration in JavaScriptCore to be + * the small heap instead of the large heap. + */ + smol?: boolean; + + /** + * When `true`, the worker will keep the parent thread alive until the worker is terminated or `unref`'d. + * When `false`, the worker will not keep the parent thread alive. + * + * By default, this is `false`. + */ + ref?: boolean; + }; +} + +interface WorkerEventMap { + message: MessageEvent; + messageerror: MessageEvent; + error: ErrorEvent; + open: Event; + close: Event; +} + interface EventSource extends EventTarget { onerror: ((this: EventSource, ev: ErrorEvent) => any) | null; onmessage: ((this: EventSource, ev: MessageEvent) => any) | null; diff --git a/packages/bun-types/jsc.d.ts b/packages/bun-types/jsc.d.ts index 08e8ba680..3e7e8947c 100644 --- a/packages/bun-types/jsc.d.ts +++ b/packages/bun-types/jsc.d.ts @@ -40,6 +40,42 @@ declare module "bun:jsc" { export function drainMicrotasks(): void; /** + * Convert a JavaScript value to a binary representation that can be sent to another Bun instance. + * + * Internally, this uses the serialization format from WebKit/Safari. + * + * @param value A JavaScript value, usually an object or array, to be converted. + * @returns A SharedArrayBuffer that can be sent to another Bun instance. + * + */ + export function serialize( + value: any, + options?: { binaryMode?: "arraybuffer" }, + ): SharedArrayBuffer; + + /** + * Convert a JavaScript value to a binary representation that can be sent to another Bun instance. + * + * Internally, this uses the serialization format from WebKit/Safari. + * + * @param value A JavaScript value, usually an object or array, to be converted. + * @returns A Buffer that can be sent to another Bun instance. + */ + export function serialize( + value: any, + options?: { binaryMode: "nodebuffer" }, + ): Buffer; + + /** + * Convert an ArrayBuffer or Buffer to a JavaScript value compatible with the HTML Structured Clone Algorithm. + * + * @param value A serialized value, usually an ArrayBuffer or Buffer, to be converted. + */ + export function deserialize( + value: ArrayBufferLike | TypedArray | Buffer, + ): any; + + /** * Set the timezone used by Intl, Date, etc. * * @param timeZone A string representing the time zone to use, such as "America/Los_Angeles" diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index fbf567446..97ad056e8 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -4101,10 +4101,9 @@ pub const Timer = struct { this.poll_ref.unref(vm); this.timer.deinit(); - if (this.did_unref_timer) { - // balance double-unrefing - vm.uws_event_loop.?.num_polls += 1; - } + + // balance double unreffing in doUnref + vm.uws_event_loop.?.num_polls += @as(i32, @intFromBool(this.did_unref_timer)); this.callback.deinit(); this.arguments.deinit(); diff --git a/src/bun.js/bindings/BunJSCModule.cpp b/src/bun.js/bindings/BunJSCModule.cpp index 5809a9813..a145f51c5 100644 --- a/src/bun.js/bindings/BunJSCModule.cpp +++ b/src/bun.js/bindings/BunJSCModule.cpp @@ -24,15 +24,19 @@ #include "JavaScriptCore/DeferTermination.h" #include "JavaScriptCore/SamplingProfiler.h" #include "JavaScriptCore/VMTrapsInlines.h" +#include "SerializedScriptValue.h" +#include "ExceptionOr.h" #if ENABLE(REMOTE_INSPECTOR) #include "JavaScriptCore/RemoteInspectorServer.h" #endif #include "mimalloc.h" +#include "JSDOMConvertBase.h" using namespace JSC; using namespace WTF; +using namespace WebCore; JSC_DECLARE_HOST_FUNCTION(functionStartRemoteDebugger); JSC_DEFINE_HOST_FUNCTION(functionStartRemoteDebugger, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -525,6 +529,74 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshotForDebugging, (JSGlobalObje return JSValue::encode(JSONParse(globalObject, WTFMove(jsonString))); } +JSC_DEFINE_HOST_FUNCTION(functionSerialize, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto* globalObject = jsCast<JSDOMGlobalObject*>(lexicalGlobalObject); + JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + JSValue value = callFrame->argument(0); + JSValue optionsObject = callFrame->argument(1); + bool asNodeBuffer = false; + if (optionsObject.isObject()) { + JSC::JSObject* options = optionsObject.getObject(); + if (JSC::JSValue binaryTypeValue = options->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "binaryType"_s))) { + if (!binaryTypeValue.isString()) { + throwTypeError(globalObject, throwScope, "binaryType must be a string"_s); + return JSValue::encode(jsUndefined()); + } + + asNodeBuffer = binaryTypeValue.toWTFString(globalObject) == "nodebuffer"_s; + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + } + } + + Vector<JSC::Strong<JSC::JSObject>> transferList; + + ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList)); + + if (serialized.hasException()) { + WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); + return JSValue::encode(jsUndefined()); + } + + auto serializedValue = serialized.releaseReturnValue(); + auto arrayBuffer = serializedValue->toArrayBuffer(); + + if (asNodeBuffer) { + size_t byteLength = arrayBuffer->byteLength(); + JSC::JSUint8Array* uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTFMove(arrayBuffer), 0, byteLength); + return JSValue::encode(uint8Array); + } + + if (arrayBuffer->isShared()) { + return JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructureWithSharingMode<ArrayBufferSharingMode::Shared>(), WTFMove(arrayBuffer))); + } + + return JSValue::encode(JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), WTFMove(arrayBuffer))); +} +JSC_DEFINE_HOST_FUNCTION(functionDeserialize, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSValue value = callFrame->argument(0); + + JSValue result; + + if (auto* jsArrayBuffer = jsDynamicCast<JSArrayBuffer*>(value)) { + result = SerializedScriptValue::fromArrayBuffer(*globalObject, globalObject, jsArrayBuffer->impl(), 0, jsArrayBuffer->impl()->byteLength()); + } else if (auto* view = jsDynamicCast<JSArrayBufferView*>(value)) { + auto arrayBuffer = view->possiblySharedImpl()->possiblySharedBuffer(); + result = SerializedScriptValue::fromArrayBuffer(*globalObject, globalObject, arrayBuffer.get(), view->byteOffset(), view->byteLength()); + } else { + throwTypeError(globalObject, throwScope, "First argument must be an ArrayBuffer"_s); + return JSValue::encode(jsUndefined()); + } + + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(jsUndefined())); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + JSC::JSObject* createJSCModule(JSC::JSGlobalObject* globalObject) { VM& vm = globalObject->vm(); @@ -561,6 +633,8 @@ JSC::JSObject* createJSCModule(JSC::JSGlobalObject* globalObject) object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), 0, functionGenerateHeapSnapshotForDebugging, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "profile"_s), 0, functionRunProfiler, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "setTimeZone"_s), 0, functionSetTimeZone, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "serialize"_s), 0, functionSerialize, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "deserialize"_s), 0, functionDeserialize, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); } return object; diff --git a/src/bun.js/bindings/BunWorkerGlobalScope.cpp b/src/bun.js/bindings/BunWorkerGlobalScope.cpp new file mode 100644 index 000000000..60b615e18 --- /dev/null +++ b/src/bun.js/bindings/BunWorkerGlobalScope.cpp @@ -0,0 +1,10 @@ +#include "config.h" + +#include "BunWorkerGlobalScope.h" + +namespace Bun { +using namespace WebCore; + +WTF_MAKE_ISO_ALLOCATED_IMPL(GlobalScope); + +}
\ No newline at end of file diff --git a/src/bun.js/bindings/BunWorkerGlobalScope.h b/src/bun.js/bindings/BunWorkerGlobalScope.h new file mode 100644 index 000000000..767289716 --- /dev/null +++ b/src/bun.js/bindings/BunWorkerGlobalScope.h @@ -0,0 +1,38 @@ +#include "root.h" + +#include "EventTarget.h" +#include "ContextDestructionObserver.h" +#include "ExceptionOr.h" +#include <wtf/URL.h> +#include <wtf/HashSet.h> +#include <wtf/Lock.h> + +namespace Bun { +class GlobalScope final : public RefCounted<GlobalScope>, public EventTargetWithInlineData { + WTF_MAKE_ISO_ALLOCATED(GlobalScope); + +public: + GlobalScope(ScriptExecutionContext* context) + : EventTargetWithInlineData() + , m_context(context) + { + } + using RefCounted::deref; + using RefCounted::ref; + + static Ref<GlobalScope> create(ScriptExecutionContext* context) + { + return adoptRef(*new GlobalScope(context)); + } + + ~GlobalScope() = default; + + EventTargetInterface eventTargetInterface() const final { return EventTargetInterface::DOMWindowEventTargetInterfaceType; } + ScriptExecutionContext* scriptExecutionContext() const final { return m_context; } + void refEventTarget() final { ref(); } + void derefEventTarget() final { deref(); } + void eventListenersDidChange() final {} + + ScriptExecutionContext* m_context; +}; +}
\ No newline at end of file diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index 0293ecc35..b7b7ef16d 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -110,6 +110,16 @@ us_socket_context_t* ScriptExecutionContext::connectedWebSocketKindClientSSL() return registerWebSocketClientContext<true>(this, webSocketContextSSL()); } +ScriptExecutionContextIdentifier ScriptExecutionContext::generateIdentifier() +{ + Locker locker { allScriptExecutionContextsMapLock }; + + // ASSERT(allScriptExecutionContextsMap().contains(m_identifier)); + // allScriptExecutionContextsMap().remove(m_identifier); + + return ++lastUniqueIdentifier; +} + void ScriptExecutionContext::regenerateIdentifier() { Locker locker { allScriptExecutionContextsMapLock }; diff --git a/src/bun.js/bindings/ScriptExecutionContext.h b/src/bun.js/bindings/ScriptExecutionContext.h index fcf65f477..c3fbd7c9b 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.h +++ b/src/bun.js/bindings/ScriptExecutionContext.h @@ -81,6 +81,15 @@ public: regenerateIdentifier(); } + ScriptExecutionContext(JSC::VM* vm, JSC::JSGlobalObject* globalObject, ScriptExecutionContextIdentifier identifier) + : m_vm(vm) + , m_globalObject(globalObject) + , m_identifier(identifier) + { + } + + static ScriptExecutionContextIdentifier generateIdentifier(); + JSC::JSGlobalObject* jsGlobalObject() { return m_globalObject; @@ -170,6 +179,12 @@ public: JSC::VM& vm() { return *m_vm; } ScriptExecutionContextIdentifier identifier() const { return m_identifier; } + bool isWorker = false; + void setGlobalObject(JSC::JSGlobalObject* globalObject) + { + m_globalObject = globalObject; + } + private: JSC::VM* m_vm = nullptr; JSC::JSGlobalObject* m_globalObject = nullptr; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index e6663bfed..7c0e1668b 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -115,6 +115,8 @@ #include "BunPlugin.h" #include "JSEnvironmentVariableMap.h" #include "DOMIsoSubspaces.h" +#include "BunWorkerGlobalScope.h" +#include "JSWorker.h" #if ENABLE(REMOTE_INSPECTOR) #include "JavaScriptCore/RemoteInspectorServer.h" @@ -160,6 +162,7 @@ namespace JSCastingHelpers = JSC::JSCastingHelpers; #include "StructuredClone.h" #include "JSWebSocket.h" #include "JSMessageEvent.h" +#include "JSEventListener.h" #include "ReadableStream.h" #include "JSSink.h" @@ -475,9 +478,9 @@ static String computeErrorInfo(JSC::VM& vm, Vector<StackFrame>& stackTrace, unsi } extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObjectClass, int count, - void* console_client) + void* console_client, int32_t executionContextId, bool miniMode) { - auto heapSize = JSC::HeapType::Large; + auto heapSize = miniMode ? JSC::HeapType::Small : JSC::HeapType::Large; JSC::VM& vm = JSC::VM::create(heapSize).leakRef(); @@ -487,7 +490,21 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObje WebCore::JSVMClientData::create(&vm); JSC::JSLockHolder locker(vm); - Zig::GlobalObject* globalObject = Zig::GlobalObject::create(vm, Zig::GlobalObject::createStructure(vm, JSC::JSGlobalObject::create(vm, JSC::JSGlobalObject::createStructure(vm, JSC::jsNull())), JSC::jsNull())); + Zig::GlobalObject* globalObject; + + if (UNLIKELY(executionContextId > -1)) { + ScriptExecutionContext* context = new WebCore::ScriptExecutionContext(&vm, nullptr, static_cast<ScriptExecutionContextIdentifier>(executionContextId)); + globalObject = Zig::GlobalObject::create( + vm, + Zig::GlobalObject::createStructure(vm, JSC::JSGlobalObject::create(vm, JSC::JSGlobalObject::createStructure(vm, JSC::jsNull())), JSC::jsNull()), + context); + } else { + globalObject = Zig::GlobalObject::create( + vm, + Zig::GlobalObject::createStructure(vm, JSC::JSGlobalObject::create(vm, JSC::JSGlobalObject::createStructure(vm, JSC::jsNull())), + JSC::jsNull())); + } + globalObject->setConsole(globalObject); globalObject->isThreadLocalDefaultGlobalObject = true; globalObject->setStackTraceLimit(DEFAULT_ERROR_STACK_TRACE_LIMIT); // Node.js defaults to 10 @@ -752,10 +769,26 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure) , m_world(WebCore::DOMWrapperWorld::create(vm, WebCore::DOMWrapperWorld::Type::Normal)) , m_worldIsNormal(true) , m_builtinInternalFunctions(vm) + , globalEventScope(Bun::GlobalScope::create(nullptr)) { - mockModule = Bun::JSMockModule::create(this); m_scriptExecutionContext = new WebCore::ScriptExecutionContext(&vm, this); + globalEventScope->m_context = m_scriptExecutionContext; + mockModule = Bun::JSMockModule::create(this); +} + +GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::ScriptExecutionContext* context) + : JSC::JSGlobalObject(vm, structure, &s_globalObjectMethodTable) + , m_bunVM(Bun__getVM()) + , m_constructors(makeUnique<WebCore::DOMConstructors>()) + , m_world(WebCore::DOMWrapperWorld::create(vm, WebCore::DOMWrapperWorld::Type::Normal)) + , m_worldIsNormal(true) + , m_builtinInternalFunctions(vm) + , globalEventScope(Bun::GlobalScope::create(context)) +{ + mockModule = Bun::JSMockModule::create(this); + m_scriptExecutionContext = context; + context->setGlobalObject(this); } GlobalObject::~GlobalObject() @@ -820,6 +853,53 @@ void GlobalObject::setConsole(void* console) #pragma mark - Globals +JSC_DEFINE_CUSTOM_GETTER(globalGetterOnMessage, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::PropertyName)) +{ + Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(JSValue::decode(thisValue)); + return JSValue::encode(eventHandlerAttribute(thisObject->eventTarget(), eventNames().messageEvent, thisObject->world())); +} + +JSC_DEFINE_CUSTOM_GETTER(globalGetterOnError, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::PropertyName)) +{ + Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(JSValue::decode(thisValue)); + return JSValue::encode(eventHandlerAttribute(thisObject->eventTarget(), eventNames().errorEvent, thisObject->world())); +} + +JSC_DEFINE_CUSTOM_SETTER(globalSetterOnMessage, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue encodedValue, JSC::PropertyName property)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + JSValue value = JSValue::decode(encodedValue); + auto* thisObject = jsCast<Zig::GlobalObject*>(JSValue::decode(thisValue)); + setEventHandlerAttribute<JSEventListener>(thisObject->eventTarget(), eventNames().messageEvent, value, *thisObject); + vm.writeBarrier(thisObject, value); + ensureStillAliveHere(value); + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(globalSetterOnError, + (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue encodedValue, JSC::PropertyName property)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + JSValue value = JSValue::decode(encodedValue); + auto* thisObject = jsCast<Zig::GlobalObject*>(JSValue::decode(thisValue)); + setEventHandlerAttribute<JSEventListener>(thisObject->eventTarget(), eventNames().errorEvent, value, *thisObject); + vm.writeBarrier(thisObject, value); + ensureStillAliveHere(value); + return true; +} + +Ref<WebCore::EventTarget> GlobalObject::eventTarget() +{ + return globalEventScope; +} + JSC_DECLARE_CUSTOM_GETTER(functionLazyLoadStreamPrototypeMap_getter); JSC_DEFINE_CUSTOM_GETTER(functionLazyLoadStreamPrototypeMap_getter, @@ -906,6 +986,9 @@ WEBCORE_GENERATED_CONSTRUCTOR_SETTER(JSURLSearchParams); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(JSDOMFormData); WEBCORE_GENERATED_CONSTRUCTOR_SETTER(JSDOMFormData); +WEBCORE_GENERATED_CONSTRUCTOR_GETTER(JSWorker); +WEBCORE_GENERATED_CONSTRUCTOR_SETTER(JSWorker); + JSC_DECLARE_CUSTOM_GETTER(JSEvent_getter); JSC_DEFINE_CUSTOM_GETTER(JSEvent_getter, @@ -3399,6 +3482,75 @@ void GlobalObject::finishCreation(VM& vm) consoleObject->putDirectBuiltinFunction(vm, this, clientData->builtinNames().writePublicName(), consoleObjectWriteCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete); } +extern "C" WebCore::Worker* WebWorker__getParentWorker(void*); +JSC_DEFINE_HOST_FUNCTION(jsFunctionPostMessage, + (JSC::JSGlobalObject * leixcalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = leixcalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + Zig::GlobalObject* globalObject = jsDynamicCast<Zig::GlobalObject*>(leixcalGlobalObject); + if (UNLIKELY(!globalObject)) + return JSValue::encode(jsUndefined()); + + Worker* worker = WebWorker__getParentWorker(globalObject->bunVM()); + if (worker == nullptr) + return JSValue::encode(jsUndefined()); + + ScriptExecutionContext* context = worker->scriptExecutionContext(); + + if (!context) + return JSValue::encode(jsUndefined()); + + auto throwScope = DECLARE_THROW_SCOPE(vm); + + JSC::JSValue value = callFrame->argument(0); + JSC::JSValue options = callFrame->argument(1); + + Vector<JSC::Strong<JSC::JSObject>> transferList; + + if (options.isObject()) { + JSC::JSObject* optionsObject = options.getObject(); + JSC::JSValue transferListValue = optionsObject->get(globalObject, vm.propertyNames->transfer); + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + if (auto* transferListArray = jsDynamicCast<JSC::JSArray*>(transferListObject)) { + for (unsigned i = 0; i < transferListArray->length(); i++) { + JSC::JSValue transferListValue = transferListArray->get(globalObject, i); + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + transferList.append(JSC::Strong<JSC::JSObject>(vm, transferListObject)); + } + } + } + } + } + + ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList)); + if (serialized.hasException()) { + WebCore::propagateException(*globalObject, throwScope, serialized.releaseException()); + return JSValue::encode(jsUndefined()); + } + + ScriptExecutionContext::postTaskTo(context->identifier(), [message = serialized.releaseReturnValue(), protectedThis = Ref { *worker }](ScriptExecutionContext& context) { + Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(context.jsGlobalObject()); + bool didFail = false; + JSValue value = message->deserialize(*globalObject, globalObject, SerializationErrorMode::NonThrowing, &didFail); + message->deref(); + + if (didFail) { + protectedThis->dispatchEvent(MessageEvent::create(eventNames().messageerrorEvent, MessageEvent::Init {}, MessageEvent::IsTrusted::Yes)); + return; + } + + WebCore::MessageEvent::Init init; + init.data = value; + protectedThis->dispatchEvent(MessageEvent::create(eventNames().messageEvent, WTFMove(init), MessageEvent::IsTrusted::Yes)); + }); + + return JSValue::encode(jsUndefined()); +} + JSC_DEFINE_HOST_FUNCTION(functionBunPeek, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -3711,94 +3863,128 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) auto& builtinNames = WebCore::builtinNames(vm); WTF::Vector<GlobalPropertyInfo> extraStaticGlobals; - extraStaticGlobals.reserveCapacity(45); + extraStaticGlobals.reserveCapacity(49); JSC::Identifier queueMicrotaskIdentifier = JSC::Identifier::fromString(vm, "queueMicrotask"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { JSC::Identifier::fromString(vm, "fetch"_s), - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 2, + JSC::JSFunction::create(vm, this, 2, "fetch"_s, Bun__fetch, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { queueMicrotaskIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 2, + JSC::JSFunction::create(vm, this, 2, "queueMicrotask"_s, functionQueueMicrotask, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { JSC::Identifier::fromString(vm, "setImmediate"_s), - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "setImmediate"_s, functionSetImmediate, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { JSC::Identifier::fromString(vm, "clearImmediate"_s), - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "clearImmediate"_s, functionClearTimeout, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { JSC::Identifier::fromString(vm, "structuredClone"_s), - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 2, + JSC::JSFunction::create(vm, this, 2, "structuredClone"_s, functionStructuredClone, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier setTimeoutIdentifier = JSC::Identifier::fromString(vm, "setTimeout"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { setTimeoutIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "setTimeout"_s, functionSetTimeout, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier clearTimeoutIdentifier = JSC::Identifier::fromString(vm, "clearTimeout"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { clearTimeoutIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "clearTimeout"_s, functionClearTimeout, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier setIntervalIdentifier = JSC::Identifier::fromString(vm, "setInterval"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { setIntervalIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "setInterval"_s, functionSetInterval, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier clearIntervalIdentifier = JSC::Identifier::fromString(vm, "clearInterval"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { clearIntervalIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "clearInterval"_s, functionClearInterval, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier atobIdentifier = JSC::Identifier::fromString(vm, "atob"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { atobIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "atob"_s, functionATOB, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier btoaIdentifier = JSC::Identifier::fromString(vm, "btoa"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { btoaIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "btoa"_s, functionBTOA, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); JSC::Identifier reportErrorIdentifier = JSC::Identifier::fromString(vm, "reportError"_s); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { reportErrorIdentifier, - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, "reportError"_s, functionReportError, ImplementationVisibility::Public), - JSC::PropertyAttribute::DontDelete | 0 }); + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); + + { + JSC::Identifier postMessageIdentifier = JSC::Identifier::fromString(vm, "postMessage"_s); + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo { postMessageIdentifier, + JSC::JSFunction::create(vm, this, 1, + "postMessage"_s, jsFunctionPostMessage, ImplementationVisibility::Public), + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); + } + + { + JSC::Identifier addEventListener = JSC::Identifier::fromString(vm, "addEventListener"_s); + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo { addEventListener, + JSC::JSFunction::create(vm, this, 2, + "addEventListener"_s, WebCore::jsEventTargetPrototypeFunction_addEventListener, ImplementationVisibility::Public), + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); + } + + { + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo { JSC::Identifier::fromString(vm, "dispatchEvent"_s), + JSC::JSFunction::create(vm, this, 1, + "dispatchEvent"_s, WebCore::jsEventTargetPrototypeFunction_dispatchEvent, ImplementationVisibility::Public), + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); + } + + { + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo { JSC::Identifier::fromString(vm, "removeEventListener"_s), + JSC::JSFunction::create(vm, this, 1, + "removeEventListener"_s, WebCore::jsEventTargetPrototypeFunction_removeEventListener, ImplementationVisibility::Public), + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); + } extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { builtinNames.startDirectStreamPrivateName(), - JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 1, + JSC::JSFunction::create(vm, this, 1, String(), functionStartDirectStream, ImplementationVisibility::Public), JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0 }); static NeverDestroyed<const String> BunLazyString(MAKE_STATIC_STRING_IMPL("Bun.lazy")); static NeverDestroyed<const String> CommonJSSymbolKey(MAKE_STATIC_STRING_IMPL("CommonJS")); JSC::Identifier BunLazyIdentifier = JSC::Identifier::fromUid(vm.symbolRegistry().symbolForKey(BunLazyString)); - JSC::JSFunction* lazyLoadFunction = JSC::JSFunction::create(vm, JSC::jsCast<JSC::JSGlobalObject*>(globalObject()), 0, + JSC::JSFunction* lazyLoadFunction = JSC::JSFunction::create(vm, this, 0, BunLazyString, functionLazyLoad, ImplementationVisibility::Public); extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { BunLazyIdentifier, @@ -3922,6 +4108,9 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "EventSource"_s), JSC::CustomGetterSetter::create(vm, EventSource_getter, EventSource_setter), 0); + putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "onmessage"_s), JSC::CustomGetterSetter::create(vm, globalGetterOnMessage, globalSetterOnMessage), 0); + putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "onerror"_s), JSC::CustomGetterSetter::create(vm, globalGetterOnError, globalSetterOnError), 0); + auto bufferAccessor = JSC::CustomGetterSetter::create(vm, JSBuffer_getter, JSBuffer_setter); auto realBufferAccessor = JSC::CustomGetterSetter::create(vm, JSBuffer_privateGetter, nullptr); @@ -3937,6 +4126,7 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) PUT_WEBCORE_GENERATED_CONSTRUCTOR("WebSocket"_s, JSWebSocket); PUT_WEBCORE_GENERATED_CONSTRUCTOR("Headers"_s, JSFetchHeaders); PUT_WEBCORE_GENERATED_CONSTRUCTOR("URLSearchParams"_s, JSURLSearchParams); + PUT_WEBCORE_GENERATED_CONSTRUCTOR("Worker"_s, JSWorker); putDirectCustomAccessor(vm, builtinNames.TransformStreamPublicName(), CustomGetterSetter::create(vm, jsServiceWorkerGlobalScope_TransformStreamConstructor, nullptr), attributesForStructure(static_cast<unsigned>(JSC::PropertyAttribute::DontEnum))); putDirectCustomAccessor(vm, builtinNames.TransformStreamPrivateName(), CustomGetterSetter::create(vm, jsServiceWorkerGlobalScope_TransformStreamConstructor, nullptr), attributesForStructure(static_cast<unsigned>(JSC::PropertyAttribute::DontEnum))); @@ -4287,6 +4477,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_JSTextEncoderSetterValue); visitor.append(thisObject->m_JSURLSearchParamsSetterValue); visitor.append(thisObject->m_JSDOMFormDataSetterValue); + visitor.append(thisObject->m_JSWorkerSetterValue); thisObject->m_JSArrayBufferSinkClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 081ca33b3..c98e499b0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -43,8 +43,13 @@ class DOMWrapperWorld; #include "BunPlugin.h" #include "JSMockFunction.h" +namespace Bun { +class GlobalScope; +} + namespace WebCore { class SubtleCrypto; +class EventTarget; } extern "C" void Bun__reportError(JSC__JSGlobalObject*, JSC__JSValue); @@ -128,6 +133,13 @@ public: return ptr; } + static GlobalObject* create(JSC::VM& vm, JSC::Structure* structure, WebCore::ScriptExecutionContext* scriptExecutionContext) + { + GlobalObject* ptr = new (NotNull, JSC::allocateCell<GlobalObject>(vm)) GlobalObject(vm, structure, scriptExecutionContext); + ptr->finishCreation(vm); + return ptr; + } + const JSDOMStructureMap& structures() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(!Thread::mayBeGCThread()); @@ -298,6 +310,9 @@ public: EncodedJSValue assignToStream(JSValue stream, JSValue controller); + Ref<WebCore::EventTarget> eventTarget(); + Ref<Bun::GlobalScope> globalEventScope; + enum class PromiseFunctions : uint8_t { Bun__HTTPRequestContext__onReject, Bun__HTTPRequestContext__onRejectStream, @@ -366,6 +381,7 @@ public: mutable WriteBarrier<Unknown> m_JSURLSearchParamsSetterValue; mutable WriteBarrier<Unknown> m_JSWebSocketSetterValue; mutable WriteBarrier<Unknown> m_JSDOMFormDataSetterValue; + mutable WriteBarrier<Unknown> m_JSWorkerSetterValue; mutable WriteBarrier<Unknown> m_BunCommonJSModuleValue; mutable WriteBarrier<JSFunction> m_thenables[promiseFunctionsSize + 1]; @@ -430,6 +446,7 @@ private: friend void WebCore::JSBuiltinInternalFunctions::initialize(Zig::GlobalObject&); WebCore::JSBuiltinInternalFunctions m_builtinInternalFunctions; GlobalObject(JSC::VM& vm, JSC::Structure* structure); + GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::ScriptExecutionContext*); std::unique_ptr<WebCore::DOMConstructors> m_constructors; uint8_t m_worldIsNormal; JSDOMStructureMap m_structures WTF_GUARDED_BY_LOCK(m_gcLock); diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index e9e9d3a8d..958a7ff20 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -40,8 +40,14 @@ pub const ZigGlobalObject = extern struct { pub const namespace = shim.namespace; pub const Interface: type = NewGlobalObject(JS.VirtualMachine); - pub fn create(class_ref: [*]CAPI.JSClassRef, count: i32, console: *anyopaque) *JSGlobalObject { - var global = shim.cppFn("create", .{ class_ref, count, console }); + pub fn create( + class_ref: [*]CAPI.JSClassRef, + count: i32, + console: *anyopaque, + context_id: i32, + mini_mode: bool, + ) *JSGlobalObject { + var global = shim.cppFn("create", .{ class_ref, count, console, context_id, mini_mode }); Backtrace.reloadHandlers() catch unreachable; return global; } @@ -959,6 +965,9 @@ pub const ZigConsoleClient = struct { _, }; + var stderr_mutex: bun.Lock = bun.Lock.init(); + var stdout_mutex: bun.Lock = bun.Lock.init(); + /// https://console.spec.whatwg.org/#formatter pub fn messageWithTypeAndLevel( //console_: ZigConsoleClient.Type, @@ -976,6 +985,21 @@ pub const ZigConsoleClient = struct { var console = global.bunVM().console; + // Lock/unlock a mutex incase two JS threads are console.log'ing at the same time + // We do this the slightly annoying way to avoid assigning a pointer + if (level == .Warning or level == .Error or message_type == .Assert) { + stderr_mutex.lock(); + } else { + stdout_mutex.lock(); + } + defer { + if (level == .Warning or level == .Error or message_type == .Assert) { + stderr_mutex.unlock(); + } else { + stdout_mutex.unlock(); + } + } + if (message_type == .Clear) { Output.resetTerminal(); return; @@ -2505,14 +2529,30 @@ pub const ZigConsoleClient = struct { } }, .ErrorEvent => { - writer.print( - comptime Output.prettyFmt("<r><blue>error<d>:<r>\n", enable_ansi_colors), - .{}, - ); + { + const error_value = value.get(this.globalThis, "error").?; - const data = value.get(this.globalThis, "error").?; - const tag = Tag.getAdvanced(data, this.globalThis, .{ .hide_global = true }); - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + if (!error_value.isEmptyOrUndefinedOrNull()) { + writer.print( + comptime Output.prettyFmt("<r><blue>error<d>:<r> ", enable_ansi_colors), + .{}, + ); + + const tag = Tag.getAdvanced(error_value, this.globalThis, .{ .hide_global = true }); + this.format(tag, Writer, writer_, error_value, this.globalThis, enable_ansi_colors); + } + } + + const message_value = value.get(this.globalThis, "message").?; + if (message_value.isString()) { + writer.print( + comptime Output.prettyFmt("<r><blue>message<d>:<r> ", enable_ansi_colors), + .{}, + ); + + const tag = Tag.getAdvanced(message_value, this.globalThis, .{ .hide_global = true }); + this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + } }, else => unreachable, } diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 1a6a40c9f..8a03d5233 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -578,7 +578,7 @@ ZIG_DECL JSC__JSValue Crypto__timingSafeEqual__slowpath(JSC__JSGlobalObject* arg #pragma mark - Zig::GlobalObject -CPP_DECL JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* arg0, int32_t arg1, void* arg2); +CPP_DECL JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* arg0, int32_t arg1, void* arg2, int32_t arg3, bool arg4); CPP_DECL void* Zig__GlobalObject__getModuleRegistryMap(JSC__JSGlobalObject* arg0); CPP_DECL bool Zig__GlobalObject__resetModuleRegistryMap(JSC__JSGlobalObject* arg0, void* arg1); diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index be2e4a626..343939cc2 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -350,7 +350,7 @@ pub extern fn Reader__intptr__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC_ pub extern fn Crypto__getRandomValues__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; pub extern fn Crypto__randomUUID__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; pub extern fn Crypto__timingSafeEqual__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; -pub extern fn Zig__GlobalObject__create(arg0: [*c]JSClassRef, arg1: i32, arg2: ?*anyopaque) *bindings.JSGlobalObject; +pub extern fn Zig__GlobalObject__create(arg0: [*c]JSClassRef, arg1: i32, arg2: ?*anyopaque, arg3: i32, arg4: bool) *bindings.JSGlobalObject; pub extern fn Zig__GlobalObject__getModuleRegistryMap(arg0: *bindings.JSGlobalObject) ?*anyopaque; pub extern fn Zig__GlobalObject__resetModuleRegistryMap(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) bool; pub extern fn Bun__Path__create(arg0: *bindings.JSGlobalObject, arg1: bool) JSC__JSValue; diff --git a/src/bun.js/bindings/webcore/ActiveDOMObject.h b/src/bun.js/bindings/webcore/ActiveDOMObject.h index e483a46b7..52daebc6f 100644 --- a/src/bun.js/bindings/webcore/ActiveDOMObject.h +++ b/src/bun.js/bindings/webcore/ActiveDOMObject.h @@ -100,7 +100,7 @@ // return adoptRef(*new PendingActivity<T>(thisObject)); // } -// bool isContextStopped() const; +// bool isContextStopped() const; // bool isAllowedToRunScript() const; // template<typename T> diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 82a2c6a24..229897fad 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -859,7 +859,7 @@ public: // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWebFakeXRInputController; // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWebXRTest; // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForDedicatedWorkerGlobalScope; - // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWorker; + std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWorker; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWorkerGlobalScope; // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWorkerLocation; // std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForExtendableEvent; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index f1b290d25..682f27980 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -852,7 +852,7 @@ public: // std::unique_ptr<IsoSubspace> m_subspaceForWebFakeXRInputController; // std::unique_ptr<IsoSubspace> m_subspaceForWebXRTest; // std::unique_ptr<IsoSubspace> m_subspaceForDedicatedWorkerGlobalScope; - // std::unique_ptr<IsoSubspace> m_subspaceForWorker; + std::unique_ptr<IsoSubspace> m_subspaceForWorker; std::unique_ptr<IsoSubspace> m_subspaceForWorkerGlobalScope; // std::unique_ptr<IsoSubspace> m_subspaceForWorkerLocation; // std::unique_ptr<IsoSubspace> m_subspaceForExtendableEvent; diff --git a/src/bun.js/bindings/webcore/EventTargetFactory.cpp b/src/bun.js/bindings/webcore/EventTargetFactory.cpp index b52214fe0..97e66977b 100644 --- a/src/bun.js/bindings/webcore/EventTargetFactory.cpp +++ b/src/bun.js/bindings/webcore/EventTargetFactory.cpp @@ -63,8 +63,8 @@ JSC::JSValue toJS(JSC::JSGlobalObject* state, JSDOMGlobalObject* globalObject, E // return toJS(state, globalObject, static_cast<Clipboard&>(impl)); // case DOMApplicationCacheEventTargetInterfaceType: // return toJS(state, globalObject, static_cast<DOMApplicationCache&>(impl)); - // case DOMWindowEventTargetInterfaceType: - // return toJS(state, globalObject, static_cast<DOMWindow&>(impl)); + case DOMWindowEventTargetInterfaceType: + return globalObject; // case DedicatedWorkerGlobalScopeEventTargetInterfaceType: // return toJS(state, globalObject, static_cast<DedicatedWorkerGlobalScope&>(impl)); // case EventSourceEventTargetInterfaceType: @@ -265,8 +265,8 @@ JSC::JSValue toJS(JSC::JSGlobalObject* state, JSDOMGlobalObject* globalObject, E // case WebXRSystemEventTargetInterfaceType: // return toJS(state, globalObject, static_cast<WebXRSystem&>(impl)); // #endif - // case WorkerEventTargetInterfaceType: - // return toJS(state, globalObject, static_cast<Worker&>(impl)); + case WorkerEventTargetInterfaceType: + return toJS(state, globalObject, static_cast<Worker&>(impl)); // case WorkletGlobalScopeEventTargetInterfaceType: // return toJS(state, globalObject, static_cast<WorkletGlobalScope&>(impl)); // case XMLHttpRequestEventTargetInterfaceType: diff --git a/src/bun.js/bindings/webcore/EventTargetHeaders.h b/src/bun.js/bindings/webcore/EventTargetHeaders.h index ae6ee0d3f..b36e49a66 100644 --- a/src/bun.js/bindings/webcore/EventTargetHeaders.h +++ b/src/bun.js/bindings/webcore/EventTargetHeaders.h @@ -254,8 +254,8 @@ // #include "JSWebXRSystem.h" // #include "WebXRSystem.h" // #endif -// #include "Worker.h" -// #include "JSWorker.h" +#include "Worker.h" +#include "JSWorker.h" // #include "WorkletGlobalScope.h" // #include "JSWorkletGlobalScope.h" // #include "XMLHttpRequest.h" @@ -263,4 +263,6 @@ // #include "XMLHttpRequestUpload.h" // #include "JSXMLHttpRequestUpload.h" +#include "BunWorkerGlobalScope.h" + #endif // EventTargetHeaders_h diff --git a/src/bun.js/bindings/webcore/JSEventTarget.cpp b/src/bun.js/bindings/webcore/JSEventTarget.cpp index 4f40b5397..9f97ac807 100644 --- a/src/bun.js/bindings/webcore/JSEventTarget.cpp +++ b/src/bun.js/bindings/webcore/JSEventTarget.cpp @@ -59,12 +59,6 @@ namespace WebCore { using namespace JSC; -// Functions - -static JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_addEventListener); -static JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_removeEventListener); -static JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_dispatchEvent); - // Attributes static JSC_DECLARE_CUSTOM_GETTER(jsEventTargetConstructor); diff --git a/src/bun.js/bindings/webcore/JSEventTarget.h b/src/bun.js/bindings/webcore/JSEventTarget.h index ac19a2d35..ee2e34611 100644 --- a/src/bun.js/bindings/webcore/JSEventTarget.h +++ b/src/bun.js/bindings/webcore/JSEventTarget.h @@ -27,6 +27,10 @@ namespace WebCore { +JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_addEventListener); +JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_removeEventListener); +JSC_DECLARE_HOST_FUNCTION(jsEventTargetPrototypeFunction_dispatchEvent); + class JSEventTarget : public JSDOMWrapper<EventTarget> { public: using Base = JSDOMWrapper<EventTarget>; diff --git a/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp b/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp index 670cac93e..75ee90c9f 100644 --- a/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp +++ b/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp @@ -55,8 +55,8 @@ EventTarget* JSEventTarget::toWrapped(VM& vm, JSValue value) // return &jsCast<JSWindowProxy*>(asObject(value))->wrapped(); // if (value.inherits<JSDOMWindow>()) // return &jsCast<JSDOMWindow*>(asObject(value))->wrapped(); - // if (value.inherits<JSDOMGlobalObject>()) - // return &jsCast<JSDOMGlobalObject*>(asObject(value))->wrapped(); + if (value.inherits<JSDOMGlobalObject>()) + return jsCast<JSDOMGlobalObject*>(asObject(value))->eventTarget().ptr(); if (value.inherits<JSEventTarget>()) return &jsCast<JSEventTarget*>(asObject(value))->wrapped(); return nullptr; @@ -66,6 +66,18 @@ std::unique_ptr<JSEventTargetWrapper> jsEventTargetCast(VM& vm, JSValue thisValu { if (auto* target = jsDynamicCast<JSEventTarget*>(thisValue)) return makeUnique<JSEventTargetWrapper>(target->wrapped(), *target); + if (!thisValue.isObject()) + return nullptr; + + JSObject* object = thisValue.getObject(); + if (object->type() == GlobalProxyType) { + object = jsCast<JSGlobalProxy*>(object)->target(); + if (!object) + return nullptr; + } + if (auto* global = jsDynamicCast<Zig::GlobalObject*>(object)) + return makeUnique<JSEventTargetWrapper>(global->eventTarget(), *global); + // if (auto* window = toJSDOMGlobalObject<JSDOMGlobalObject>(vm, thisValue)) // return makeUnique<JSEventTargetWrapper>(*window, *window); // if (auto* scope = toJSDOMGlobalObject<JSWorkerGlobalScope>(vm, thisValue)) diff --git a/src/bun.js/bindings/webcore/JSWorker.cpp b/src/bun.js/bindings/webcore/JSWorker.cpp new file mode 100644 index 000000000..68e06a2a0 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSWorker.cpp @@ -0,0 +1,524 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "JSWorker.h" + +#include "ActiveDOMObject.h" +#include "EventNames.h" +#include "ExtendedDOMClientIsoSubspaces.h" +#include "ExtendedDOMIsoSubspaces.h" +#include "IDLTypes.h" +#include "JSDOMAttribute.h" +#include "JSDOMBinding.h" +#include "JSDOMConstructor.h" +#include "JSDOMConvertAny.h" +#include "JSDOMConvertBase.h" +#include "JSDOMConvertDictionary.h" +#include "JSDOMConvertInterface.h" +#include "JSDOMConvertObject.h" +#include "JSDOMConvertSequences.h" +#include "JSDOMConvertStrings.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMOperation.h" +#include "JSDOMWrapperCache.h" +#include "JSEventListener.h" +#include "StructuredSerializeOptions.h" +#include "JSWorkerOptions.h" +#include "ScriptExecutionContext.h" +#include "WebCoreJSClientData.h" +#include <JavaScriptCore/HeapAnalyzer.h> +#include <JavaScriptCore/IteratorOperations.h> +#include <JavaScriptCore/JSArray.h> +#include <JavaScriptCore/JSCInlines.h> +#include <JavaScriptCore/JSDestructibleObjectHeapCellType.h> +#include <JavaScriptCore/SlotVisitorMacros.h> +#include <JavaScriptCore/SubspaceInlines.h> +#include <wtf/GetPtr.h> +#include <wtf/PointerPreparations.h> +#include <wtf/URL.h> + +namespace WebCore { +using namespace JSC; + +// Functions + +static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_terminate); +static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_postMessage); +static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_unref); +static JSC_DECLARE_HOST_FUNCTION(jsWorkerPrototypeFunction_ref); + +// Attributes + +static JSC_DECLARE_CUSTOM_GETTER(jsWorkerConstructor); +static JSC_DECLARE_CUSTOM_GETTER(jsWorker_onmessage); +static JSC_DECLARE_CUSTOM_SETTER(setJSWorker_onmessage); +static JSC_DECLARE_CUSTOM_GETTER(jsWorker_onmessageerror); +static JSC_DECLARE_CUSTOM_SETTER(setJSWorker_onmessageerror); +static JSC_DECLARE_CUSTOM_GETTER(jsWorker_onerror); +static JSC_DECLARE_CUSTOM_SETTER(setJSWorker_onerror); + +class JSWorkerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSWorkerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSWorkerPrototype* ptr = new (NotNull, JSC::allocateCell<JSWorkerPrototype>(vm)) JSWorkerPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template<typename CellType, JSC::SubspaceAccess> + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWorkerPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSWorkerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWorkerPrototype, JSWorkerPrototype::Base); + +using JSWorkerDOMConstructor = JSDOMConstructor<JSWorker>; + +template<> EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + VM& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast<JSWorkerDOMConstructor*>(callFrame->jsCallee()); + ASSERT(castedThis); + if (UNLIKELY(callFrame->argumentCount() < 1)) + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + auto* context = castedThis->scriptExecutionContext(); + if (UNLIKELY(!context)) + return throwConstructorScriptExecutionContextUnavailableError(*lexicalGlobalObject, throwScope, "Worker"); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto scriptUrl = convert<IDLUSVString>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->argument(1); + + auto options = WorkerOptions {}; + options.bun.unref = true; + + if (JSObject* optionsObject = JSC::jsDynamicCast<JSC::JSObject*>(argument1.value())) { + if (auto nameValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "name"_s))) { + if (nameValue.isString()) { + options.name = nameValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + } + } + + if (auto bunValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "bun"_s))) { + if (bunValue.isObject()) { + if (auto* bunObject = bunValue.getObject()) { + if (auto miniModeValue = bunObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s))) { + options.bun.mini = miniModeValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + } + + if (auto ref = bunObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "ref"_s))) { + options.bun.unref = !ref.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + } + } + } + } + } + + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + auto object = Worker::create(*context, WTFMove(scriptUrl), WTFMove(options)); + if constexpr (IsExceptionOr<decltype(object)>) + RETURN_IF_EXCEPTION(throwScope, {}); + static_assert(TypeOrExceptionOrUnderlyingType<decltype(object)>::isRef); + auto jsValue = toJSNewlyCreated<IDLInterface<Worker>>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, WTFMove(object)); + if constexpr (IsExceptionOr<decltype(object)>) + RETURN_IF_EXCEPTION(throwScope, {}); + setSubclassStructureIfNeeded<Worker>(lexicalGlobalObject, callFrame, asObject(jsValue)); + RETURN_IF_EXCEPTION(throwScope, {}); + return JSValue::encode(jsValue); +} +JSC_ANNOTATE_HOST_FUNCTION(JSWorkerDOMConstructorConstruct, JSWorkerDOMConstructor::construct); + +template<> const ClassInfo JSWorkerDOMConstructor::s_info = { "Worker"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWorkerDOMConstructor) }; + +template<> JSValue JSWorkerDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return JSEventTarget::getConstructor(vm, &globalObject); +} + +template<> void JSWorkerDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "Worker"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSWorker::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +/* Hash table for prototype */ + +static const HashTableValue JSWorkerPrototypeTableValues[] = { + { "constructor"_s, static_cast<unsigned>(PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWorkerConstructor, 0 } }, + { "onmessage"_s, JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsWorker_onmessage, setJSWorker_onmessage } }, + { "onmessageerror"_s, JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsWorker_onmessageerror, setJSWorker_onmessageerror } }, + { "onerror"_s, JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsWorker_onerror, setJSWorker_onerror } }, + { "terminate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_terminate, 0 } }, + { "postMessage"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_postMessage, 1 } }, + { "ref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_ref, 0 } }, + { "unref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWorkerPrototypeFunction_unref, 0 } }, +}; + +const ClassInfo JSWorkerPrototype::s_info = { "Worker"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWorkerPrototype) }; + +void JSWorkerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSWorker::info(), JSWorkerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSWorker::s_info = { "Worker"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWorker) }; + +JSWorker::JSWorker(Structure* structure, JSDOMGlobalObject& globalObject, Ref<Worker>&& impl) + : JSEventTarget(structure, globalObject, WTFMove(impl)) +{ +} + +// static_assert(std::is_base_of<ActiveDOMObject, Worker>::value, "Interface is marked as [ActiveDOMObject] but implementation class does not subclass ActiveDOMObject."); + +JSObject* JSWorker::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSWorkerPrototype::createStructure(vm, &globalObject, JSEventTarget::prototype(vm, globalObject)); + structure->setMayBePrototype(true); + return JSWorkerPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSWorker::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype<JSWorker>(vm, globalObject); +} + +JSValue JSWorker::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor<JSWorkerDOMConstructor, DOMConstructorID::Worker>(vm, *jsCast<const JSDOMGlobalObject*>(globalObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWorkerConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast<JSWorkerPrototype*>(JSValue::decode(thisValue)); + if (UNLIKELY(!prototype)) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSWorker::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); +} + +static inline JSValue jsWorker_onmessageGetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject) +{ + UNUSED_PARAM(lexicalGlobalObject); + return eventHandlerAttribute(thisObject.wrapped(), eventNames().messageEvent, worldForDOMObject(thisObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWorker_onmessage, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::get<jsWorker_onmessageGetter, CastedThisErrorBehavior::Assert>(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline bool setJSWorker_onmessageSetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + UNUSED_PARAM(vm); + setEventHandlerAttribute<JSEventListener>(thisObject.wrapped(), eventNames().messageEvent, value, thisObject); + vm.writeBarrier(&thisObject, value); + ensureStillAliveHere(value); + + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(setJSWorker_onmessage, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::set<setJSWorker_onmessageSetter>(*lexicalGlobalObject, thisValue, encodedValue, attributeName); +} + +static inline JSValue jsWorker_onmessageerrorGetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject) +{ + UNUSED_PARAM(lexicalGlobalObject); + return eventHandlerAttribute(thisObject.wrapped(), eventNames().messageerrorEvent, worldForDOMObject(thisObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWorker_onmessageerror, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::get<jsWorker_onmessageerrorGetter, CastedThisErrorBehavior::Assert>(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline bool setJSWorker_onmessageerrorSetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + UNUSED_PARAM(vm); + setEventHandlerAttribute<JSEventListener>(thisObject.wrapped(), eventNames().messageerrorEvent, value, thisObject); + vm.writeBarrier(&thisObject, value); + ensureStillAliveHere(value); + + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(setJSWorker_onmessageerror, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::set<setJSWorker_onmessageerrorSetter>(*lexicalGlobalObject, thisValue, encodedValue, attributeName); +} + +static inline JSValue jsWorker_onerrorGetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject) +{ + UNUSED_PARAM(lexicalGlobalObject); + return eventHandlerAttribute(thisObject.wrapped(), eventNames().errorEvent, worldForDOMObject(thisObject)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWorker_onerror, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::get<jsWorker_onerrorGetter, CastedThisErrorBehavior::Assert>(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline bool setJSWorker_onerrorSetter(JSGlobalObject& lexicalGlobalObject, JSWorker& thisObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + UNUSED_PARAM(vm); + setEventHandlerAttribute<JSEventListener>(thisObject.wrapped(), eventNames().errorEvent, value, thisObject); + vm.writeBarrier(&thisObject, value); + ensureStillAliveHere(value); + + return true; +} + +JSC_DEFINE_CUSTOM_SETTER(setJSWorker_onerror, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName attributeName)) +{ + return IDLAttribute<JSWorker>::set<setJSWorker_onerrorSetter>(*lexicalGlobalObject, thisValue, encodedValue, attributeName); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.terminate(); }))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWorkerPrototypeFunction_terminate, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWorker>::call<jsWorkerPrototypeFunction_terminateBody>(*lexicalGlobalObject, *callFrame, "terminate"); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_postMessage1Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto message = convert<IDLAny>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); + auto transfer = convert<IDLSequence<IDLObject>>(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.postMessage(*jsCast<JSDOMGlobalObject*>(lexicalGlobalObject), WTFMove(message), WTFMove(transfer)); }))); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_postMessage2Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto message = convert<IDLAny>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->argument(1); + JSValue optionsValue = argument1.value(); + StructuredSerializeOptions options; + if (optionsValue.isObject()) { + JSObject* optionsObject = asObject(optionsValue); + if (auto transferListValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "transfer"_s))) { + auto transferList = convert<IDLSequence<IDLObject>>(*lexicalGlobalObject, transferListValue); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + options.transfer = WTFMove(transferList); + } + } + + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.postMessage(*jsCast<JSDOMGlobalObject*>(lexicalGlobalObject), WTFMove(message), WTFMove(options)); }))); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_postMessageOverloadDispatcher(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + size_t argsCount = std::min<size_t>(2, callFrame->argumentCount()); + if (argsCount == 1) { + RELEASE_AND_RETURN(throwScope, (jsWorkerPrototypeFunction_postMessage2Body(lexicalGlobalObject, callFrame, castedThis))); + } + if (argsCount == 2) { + JSValue distinguishingArg = callFrame->uncheckedArgument(1); + if (distinguishingArg.isUndefined()) + RELEASE_AND_RETURN(throwScope, (jsWorkerPrototypeFunction_postMessage2Body(lexicalGlobalObject, callFrame, castedThis))); + if (distinguishingArg.isUndefinedOrNull()) + RELEASE_AND_RETURN(throwScope, (jsWorkerPrototypeFunction_postMessage2Body(lexicalGlobalObject, callFrame, castedThis))); + { + bool success = hasIteratorMethod(lexicalGlobalObject, distinguishingArg); + RETURN_IF_EXCEPTION(throwScope, {}); + if (success) + RELEASE_AND_RETURN(throwScope, (jsWorkerPrototypeFunction_postMessage1Body(lexicalGlobalObject, callFrame, castedThis))); + } + if (distinguishingArg.isObject()) + RELEASE_AND_RETURN(throwScope, (jsWorkerPrototypeFunction_postMessage2Body(lexicalGlobalObject, callFrame, castedThis))); + } + return argsCount < 1 ? throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)) : throwVMTypeError(lexicalGlobalObject, throwScope); +} + +JSC_DEFINE_HOST_FUNCTION(jsWorkerPrototypeFunction_postMessage, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWorker>::call<jsWorkerPrototypeFunction_postMessageOverloadDispatcher>(*lexicalGlobalObject, *callFrame, "postMessage"); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_refBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + castedThis->wrapped().setKeepAlive(true); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWorkerPrototypeFunction_ref, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWorker>::call<jsWorkerPrototypeFunction_refBody>(*lexicalGlobalObject, *callFrame, "ref"); +} + +static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_unrefBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWorker>::ClassParameter castedThis) +{ + castedThis->wrapped().setKeepAlive(false); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWorkerPrototypeFunction_unref, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWorker>::call<jsWorkerPrototypeFunction_unrefBody>(*lexicalGlobalObject, *callFrame, "unref"); +} + +JSC::GCClient::IsoSubspace* JSWorker::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl<JSWorker, UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWorker.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWorker = std::forward<decltype(space)>(space); }, + [](auto& spaces) { return spaces.m_subspaceForWorker.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWorker = std::forward<decltype(space)>(space); }); +} + +void JSWorker::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast<JSWorker*>(cell); + analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); + if (thisObject->scriptExecutionContext()) + analyzer.setLabelForCell(cell, "url "_s + thisObject->scriptExecutionContext()->url().string()); + Base::analyzeHeap(cell, analyzer); +} + +bool JSWorkerOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void*, AbstractSlotVisitor& visitor, const char** reason) +{ + auto* jsWorker = jsCast<JSWorker*>(handle.slot()->asCell()); + auto& wrapped = jsWorker->wrapped(); + if (wrapped.hasPendingActivity()) { + if (UNLIKELY(reason)) + *reason = "ActiveDOMObject with pending activity"; + return true; + } + UNUSED_PARAM(visitor); + UNUSED_PARAM(reason); + return false; +} + +void JSWorkerOwner::finalize(JSC::Handle<JSC::Unknown> handle, void* context) +{ + auto* jsWorker = static_cast<JSWorker*>(handle.slot()->asCell()); + auto& world = *static_cast<DOMWrapperWorld*>(context); + uncacheWrapper(world, &jsWorker->wrapped(), jsWorker); +} + +#if ENABLE(BINDING_INTEGRITY) +#if PLATFORM(WIN) +#pragma warning(disable : 4483) +extern "C" { +extern void (*const __identifier("??_7Worker@WebCore@@6B@")[])(); +} +#else +extern "C" { +extern void* _ZTVN7WebCore6WorkerE[]; +} +#endif +#endif + +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref<Worker>&& impl) +{ + + if constexpr (std::is_polymorphic_v<Worker>) { +#if ENABLE(BINDING_INTEGRITY) + const void* actualVTablePointer = getVTablePointer(impl.ptr()); +#if PLATFORM(WIN) + void* expectedVTablePointer = __identifier("??_7Worker@WebCore@@6B@"); +#else + void* expectedVTablePointer = &_ZTVN7WebCore6WorkerE[2]; +#endif + + // If you hit this assertion you either have a use after free bug, or + // Worker has subclasses. If Worker has subclasses that get passed + // to toJS() we currently require Worker you to opt out of binding hardening + // by adding the SkipVTableValidation attribute to the interface IDL definition + RELEASE_ASSERT(actualVTablePointer == expectedVTablePointer); +#endif + } + return createWrapper<Worker>(globalObject, WTFMove(impl)); +} + +JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Worker& impl) +{ + return wrap(lexicalGlobalObject, globalObject, impl); +} + +Worker* JSWorker::toWrapped(JSC::VM&, JSC::JSValue value) +{ + if (auto* wrapper = jsDynamicCast<JSWorker*>(value)) + return &wrapper->wrapped(); + return nullptr; +} +} diff --git a/src/bun.js/bindings/webcore/JSWorker.h b/src/bun.js/bindings/webcore/JSWorker.h new file mode 100644 index 000000000..b3b2dde76 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSWorker.h @@ -0,0 +1,99 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "JSDOMWrapper.h" +#include "JSEventTarget.h" +#include "Worker.h" +#include <wtf/NeverDestroyed.h> + +namespace WebCore { + +class JSWorker : public JSEventTarget { +public: + using Base = JSEventTarget; + using DOMWrapped = Worker; + static JSWorker* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref<Worker>&& impl) + { + JSWorker* ptr = new (NotNull, JSC::allocateCell<JSWorker>(globalObject->vm())) JSWorker(structure, *globalObject, WTFMove(impl)); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static Worker* toWrapped(JSC::VM&, JSC::JSValue); + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); + } + + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + Worker& wrapped() const + { + return static_cast<Worker&>(Base::wrapped()); + } + +protected: + JSWorker(JSC::Structure*, JSDOMGlobalObject&, Ref<Worker>&&); + + DECLARE_DEFAULT_FINISH_CREATION; +}; + +class JSWorkerOwner final : public JSC::WeakHandleOwner { +public: + bool isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown>, void* context, JSC::AbstractSlotVisitor&, const char**) final; + void finalize(JSC::Handle<JSC::Unknown>, void* context) final; +}; + +inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, Worker*) +{ + static NeverDestroyed<JSWorkerOwner> owner; + return &owner.get(); +} + +inline void* wrapperKey(Worker* wrappableObject) +{ + return wrappableObject; +} + +JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, Worker&); +inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Worker* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref<Worker>&&); +inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr<Worker>&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } + +template<> struct JSDOMWrapperConverterTraits<Worker> { + using WrapperClass = JSWorker; + using ToWrappedReturnType = Worker*; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSWorkerOptions.cpp b/src/bun.js/bindings/webcore/JSWorkerOptions.cpp new file mode 100644 index 000000000..6bee7e290 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSWorkerOptions.cpp @@ -0,0 +1,83 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "JSWorkerOptions.h" + +#include "JSDOMConvertEnumeration.h" +#include "JSDOMConvertStrings.h" +// #include "JSFetchRequestCredentials.h" +// #include "JSWorkerType.h" +#include <JavaScriptCore/JSCInlines.h> + +namespace WebCore { +using namespace JSC; + +template<> WorkerOptions convertDictionary<WorkerOptions>(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + VM& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + bool isNullOrUndefined = value.isUndefinedOrNull(); + auto* object = isNullOrUndefined ? nullptr : value.getObject(); + if (UNLIKELY(!isNullOrUndefined && !object)) { + throwTypeError(&lexicalGlobalObject, throwScope); + return {}; + } + WorkerOptions result; + // JSValue credentialsValue; + // if (isNullOrUndefined) + // credentialsValue = jsUndefined(); + // else { + // credentialsValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "credentials"_s)); + // RETURN_IF_EXCEPTION(throwScope, {}); + // } + // if (!credentialsValue.isUndefined()) { + // result.credentials = convert<IDLEnumeration<FetchRequestCredentials>>(lexicalGlobalObject, credentialsValue); + // RETURN_IF_EXCEPTION(throwScope, {}); + // } else + // result.credentials = FetchRequestCredentials::SameOrigin; + JSValue nameValue; + if (isNullOrUndefined) + nameValue = jsUndefined(); + else { + nameValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "name"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!nameValue.isUndefined()) { + result.name = convert<IDLDOMString>(lexicalGlobalObject, nameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } else + result.name = emptyString(); + // JSValue typeValue; + // if (isNullOrUndefined) + // typeValue = jsUndefined(); + // else { + // typeValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "type"_s)); + // RETURN_IF_EXCEPTION(throwScope, { }); + // } + // if (!typeValue.isUndefined()) { + // result.type = convert<IDLEnumeration<WorkerType>>(lexicalGlobalObject, typeValue); + // RETURN_IF_EXCEPTION(throwScope, { }); + // } else + // result.type = WorkerType::Classic; + return result; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSWorkerOptions.h b/src/bun.js/bindings/webcore/JSWorkerOptions.h new file mode 100644 index 000000000..afb5f3656 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSWorkerOptions.h @@ -0,0 +1,30 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "JSDOMConvertDictionary.h" +#include "WorkerOptions.h" + +namespace WebCore { + +template<> WorkerOptions convertDictionary<WorkerOptions>(JSC::JSGlobalObject&, JSC::JSValue); + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index e3a741a02..60729ea65 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -74,6 +74,7 @@ #include <JavaScriptCore/ExceptionHelpers.h> #include <JavaScriptCore/IterationKind.h> #include <JavaScriptCore/JSArrayBuffer.h> +#include <JavaScriptCore/ArrayBuffer.h> #include <JavaScriptCore/JSArrayBufferView.h> #include <JavaScriptCore/JSCInlines.h> #include <JavaScriptCore/JSDataView.h> @@ -2527,7 +2528,7 @@ public: Vector<std::unique_ptr<DetachedRTCDataChannel>>&& detachedRTCDataChannels #endif , - ArrayBufferContentsArray* arrayBufferContentsArray, const Vector<uint8_t>& buffer, const Vector<String>& blobURLs, const Vector<String> blobFilePaths, ArrayBufferContentsArray* sharedBuffers + ArrayBufferContentsArray* arrayBufferContentsArray, const Span<uint8_t>& buffer, const Vector<String>& blobURLs, const Vector<String> blobFilePaths, ArrayBufferContentsArray* sharedBuffers #if ENABLE(WEBASSEMBLY) , WasmModuleArray* wasmModules, WasmMemoryHandleArray* wasmMemoryHandles @@ -2540,7 +2541,7 @@ public: { if (!buffer.size()) return std::make_pair(jsNull(), SerializationReturnCode::UnspecifiedError); - CloneDeserializer deserializer(lexicalGlobalObject, globalObject, arrayBufferContentsArray, buffer, blobURLs, blobFilePaths, sharedBuffers + CloneDeserializer deserializer(lexicalGlobalObject, globalObject, arrayBufferContentsArray, Span<uint8_t> { buffer.begin(), buffer.end() }, blobURLs, blobFilePaths, sharedBuffers #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) , WTFMove(detachedOffscreenCanvases) @@ -2660,7 +2661,7 @@ private: // m_version = 0xFFFFFFFF; // } - CloneDeserializer(JSGlobalObject* lexicalGlobalObject, JSGlobalObject* globalObject, ArrayBufferContentsArray* arrayBufferContents, const Vector<uint8_t>& buffer + CloneDeserializer(JSGlobalObject* lexicalGlobalObject, JSGlobalObject* globalObject, ArrayBufferContentsArray* arrayBufferContents, const Span<uint8_t>& buffer #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) , Vector<std::unique_ptr<DetachedOffscreenCanvas>>&& detachedOffscreenCanvases = {} @@ -2766,7 +2767,7 @@ private: // m_version = 0xFFFFFFFF; // } - CloneDeserializer(JSGlobalObject* lexicalGlobalObject, JSGlobalObject* globalObject, ArrayBufferContentsArray* arrayBufferContents, const Vector<uint8_t>& buffer, const Vector<String>& blobURLs, const Vector<String> blobFilePaths, ArrayBufferContentsArray* sharedBuffers + CloneDeserializer(JSGlobalObject* lexicalGlobalObject, JSGlobalObject* globalObject, ArrayBufferContentsArray* arrayBufferContents, const Span<uint8_t>& buffer, const Vector<String>& blobURLs, const Vector<String> blobFilePaths, ArrayBufferContentsArray* sharedBuffers #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) , Vector<std::unique_ptr<DetachedOffscreenCanvas>>&& detachedOffscreenCanvases @@ -5282,6 +5283,73 @@ String SerializedScriptValue::toString() const return CloneDeserializer::deserializeString(m_data); } +Ref<JSC::ArrayBuffer> SerializedScriptValue::toArrayBuffer() +{ + if (this->m_data.size() == 0) { + return ArrayBuffer::create(static_cast<size_t>(0), static_cast<unsigned>(1)); + } + + this->ref(); + auto arrayBuffer = ArrayBuffer::createFromBytes( + this->m_data.data(), this->m_data.size(), createSharedTask<void(void*)>([protectedThis = Ref { *this }](void* p) { + protectedThis->deref(); + })); + + // Note: using the SharedArrayBufferContents::create function directly didn't work. + arrayBuffer->makeShared(); + + return arrayBuffer; +} + +JSC::JSValue SerializedScriptValue::fromArrayBuffer(JSC::JSGlobalObject& domGlobal, JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer* arrayBuffer, size_t byteOffset, size_t maxByteLength, SerializationErrorMode throwExceptions, bool* didFail) +{ + auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); + + if (!arrayBuffer || arrayBuffer->isDetached()) { + if (didFail) + *didFail = true; + + if (throwExceptions == SerializationErrorMode::Throwing) + throwTypeError(globalObject, throwScope, "Cannot deserialize a detached ArrayBuffer"_s); + + return JSC::jsUndefined(); + } + auto blobURLs = Vector<String> {}; + auto blobFiles = Vector<String> {}; + + if (arrayBuffer->isShared()) { + // prevent detaching while in-use + arrayBuffer->pin(); + } + + auto* data = static_cast<uint8_t*>(arrayBuffer->data()) + byteOffset; + auto size = std::min(arrayBuffer->byteLength(), maxByteLength); + auto span = Span<uint8_t> { data, size }; + + auto result = CloneDeserializer::deserialize(&domGlobal, globalObject, nullptr, span, blobURLs, blobFiles, nullptr +#if ENABLE(WEBASSEMBLY) + , + nullptr, nullptr +#endif +#if ENABLE(WEB_CODECS) + , + WTFMove(m_serializedVideoChunks), WTFMove(m_serializedVideoFrames) +#endif + ); + + if (arrayBuffer->isShared()) { + arrayBuffer->unpin(); + } + + if (didFail) { + *didFail = result.second != SerializationReturnCode::SuccessfullyCompleted; + } + if (throwExceptions == SerializationErrorMode::Throwing) + maybeThrowExceptionIfSerializationFailed(*globalObject, result.second); + + return result.first ? result.first : jsNull(); +} + // JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject, JSGlobalObject* globalObject, SerializationErrorMode throwExceptions, bool* didFail) // { // return deserialize(lexicalGlobalObject, globalObject, {}, throwExceptions, didFail); @@ -5396,7 +5464,8 @@ JSValueRef SerializedScriptValue::deserialize(JSContextRef destinationContext, J return toRef(lexicalGlobalObject, value); } -Ref<SerializedScriptValue> SerializedScriptValue::nullValue() +Ref<SerializedScriptValue> +SerializedScriptValue::nullValue() { return adoptRef(*new SerializedScriptValue(Vector<uint8_t>())); } diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.h b/src/bun.js/bindings/webcore/SerializedScriptValue.h index 6e2cbae86..189945d52 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.h +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.h @@ -108,6 +108,9 @@ public: WEBCORE_EXPORT String toString() const; + WEBCORE_EXPORT Ref<JSC::ArrayBuffer> toArrayBuffer(); + static JSC::JSValue fromArrayBuffer(JSC::JSGlobalObject&, JSC::JSGlobalObject*, JSC::ArrayBuffer* arrayBuffer, size_t byteOffset = 0, size_t maxByteLength = 0, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr); + // API implementation helpers. These don't expose special behavior for ArrayBuffers or MessagePorts. WEBCORE_EXPORT static RefPtr<SerializedScriptValue> create(JSContextRef, JSValueRef, JSValueRef* exception); WEBCORE_EXPORT JSValueRef deserialize(JSContextRef, JSValueRef* exception); diff --git a/src/bun.js/bindings/webcore/StructuredSerializeOptions.h b/src/bun.js/bindings/webcore/StructuredSerializeOptions.h new file mode 100644 index 000000000..c1470f94a --- /dev/null +++ b/src/bun.js/bindings/webcore/StructuredSerializeOptions.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019-2021 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <JavaScriptCore/JSObject.h> +#include <JavaScriptCore/Strong.h> +#include <wtf/Vector.h> + +namespace WebCore { + +struct StructuredSerializeOptions { + StructuredSerializeOptions() = default; + StructuredSerializeOptions(Vector<JSC::Strong<JSC::JSObject>>&& transfer) + : transfer(WTFMove(transfer)) + { + } + + Vector<JSC::Strong<JSC::JSObject>> transfer; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp new file mode 100644 index 000000000..fe7dc2544 --- /dev/null +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2008-2017 Apple Inc. All rights reserved. + * Copyright (C) 2009 Google Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "Worker.h" + +// #include "ContentSecurityPolicy.h" +// #include "DedicatedWorkerGlobalScope.h" +#include "ErrorEvent.h" +#include "Event.h" +#include "EventNames.h" +// #include "InspectorInstrumentation.h" +// #include "LoaderStrategy.h" +// #include "PlatformStrategies.h" +#if ENABLE(WEB_RTC) +#include "RTCRtpScriptTransform.h" +#include "RTCRtpScriptTransformer.h" +#endif +// #include "ResourceResponse.h" +// #include "SecurityOrigin.h" +#include "StructuredSerializeOptions.h" +// #include "WorkerGlobalScopeProxy.h" +// #include "WorkerInitializationData.h" +// #include "WorkerScriptLoader.h" +// #include "WorkerThread.h" +#include <JavaScriptCore/IdentifiersFactory.h> +#include <JavaScriptCore/ScriptCallStack.h> +#include <wtf/HashSet.h> +#include <wtf/IsoMallocInlines.h> +#include <wtf/MainThread.h> +#include <wtf/NeverDestroyed.h> +#include <wtf/Scope.h> +#include "SerializedScriptValue.h" +#include "ScriptExecutionContext.h" +#include "JavaScriptCore/JSMap.h" +#include "JavaScriptCore/JSModuleLoader.h" +#include "JavaScriptCore/DeferredWorkTimer.h" +#include "MessageEvent.h" +#include <JavaScriptCore/HashMapImplInlines.h> + +namespace WebCore { + +WTF_MAKE_ISO_ALLOCATED_IMPL(Worker); + +extern "C" void WebWorker__terminate( + void* worker); + +static Lock allWorkersLock; +static HashMap<ScriptExecutionContextIdentifier, Worker*>& allWorkers() WTF_REQUIRES_LOCK(allWorkersLock) +{ + static NeverDestroyed<HashMap<ScriptExecutionContextIdentifier, Worker*>> map; + return map; +} + +void Worker::networkStateChanged(bool isOnline) +{ + // Locker locker { allWorkersLock }; + // for (auto& contextIdentifier : allWorkers().keys()) { + // ScriptExecutionContext::postTaskTo(contextIdentifier, [isOnline](auto& context) { + // auto& globalScope = downcast<WorkerGlobalScope>(context); + // globalScope.setIsOnline(isOnline); + // globalScope.dispatchEvent(Event::create(isOnline ? eventNames().onlineEvent : eventNames().offlineEvent, Event::CanBubble::No, Event::IsCancelable::No)); + // }); + // } +} + +Worker::Worker(ScriptExecutionContext& context, WorkerOptions&& options) + : EventTargetWithInlineData() + , ContextDestructionObserver(&context) + , m_options(WTFMove(options)) + , m_identifier("worker:" + Inspector::IdentifiersFactory::createIdentifier()) + , m_clientIdentifier(ScriptExecutionContext::generateIdentifier()) +{ + // static bool addedListener; + // if (!addedListener) { + // platformStrategies()->loaderStrategy()->addOnlineStateChangeListener(&networkStateChanged); + // addedListener = true; + // } + + Locker locker { allWorkersLock }; + auto addResult = allWorkers().add(m_clientIdentifier, this); + ASSERT_UNUSED(addResult, addResult.isNewEntry); +} + +extern "C" void* WebWorker__create( + Worker* worker, + void* parent, + BunString name, + BunString url, + BunString* errorMessage, + uint32_t parentContextId, + uint32_t contextId, + bool miniMode, + bool unrefByDefault); +extern "C" void WebWorker__setRef( + void* worker, + bool ref); + +void Worker::setKeepAlive(bool keepAlive) +{ + WebWorker__setRef(impl_, keepAlive); +} + +ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const String& urlInit, WorkerOptions&& options) +{ + auto worker = adoptRef(*new Worker(context, WTFMove(options))); + + WTF::String url = urlInit; + if (url.startsWith("file://"_s)) { + url = WTF::URL(url).fileSystemPath(); + } + BunString urlStr = Bun::toString(url); + BunString errorMessage = BunStringEmpty; + BunString nameStr = Bun::toString(worker->m_options.name); + + bool miniMode = worker->m_options.bun.mini; + bool unrefByDefault = worker->m_options.bun.unref; + + void* impl = WebWorker__create( + worker.ptr(), + jsCast<Zig::GlobalObject*>(context.jsGlobalObject())->bunVM(), + nameStr, + urlStr, + &errorMessage, + static_cast<uint32_t>(context.identifier()), + static_cast<uint32_t>(worker->m_clientIdentifier), miniMode, unrefByDefault); + + if (!impl) { + return Exception { TypeError, Bun::toWTFString(errorMessage) }; + } + + worker->impl_ = impl; + worker->m_workerCreationTime = MonotonicTime::now(); + + return worker; +} + +Worker::~Worker() +{ + { + Locker locker { allWorkersLock }; + allWorkers().remove(m_clientIdentifier); + } + // m_contextProxy.workerObjectDestroyed(); +} + +ExceptionOr<void> Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue messageValue, StructuredSerializeOptions&& options) +{ + if (m_wasTerminated) + return Exception { InvalidStateError, "Worker has been terminated"_s }; + + auto message = SerializedScriptValue::create(state, messageValue, WTFMove(options.transfer), SerializationForStorage::No, SerializationContext::WorkerPostMessage); + if (message.hasException()) + return message.releaseException(); + + this->postTaskToWorkerGlobalScope([message = message.releaseReturnValue()](auto& context) { + Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(context.jsGlobalObject()); + bool didFail = false; + JSValue value = message->deserialize(*globalObject, globalObject, SerializationErrorMode::NonThrowing, &didFail); + message->deref(); + + if (didFail) { + globalObject->eventTarget()->dispatchEvent(MessageEvent::create(eventNames().messageerrorEvent, MessageEvent::Init {}, MessageEvent::IsTrusted::Yes)); + return; + } + + WebCore::MessageEvent::Init init; + init.data = value; + globalObject->eventTarget()->dispatchEvent(MessageEvent::create(eventNames().messageEvent, WTFMove(init), MessageEvent::IsTrusted::Yes)); + }); + return {}; +} + +void Worker::terminate() +{ + // m_contextProxy.terminateWorkerGlobalScope(); + m_wasTerminated = true; + WebWorker__terminate(impl_); +} + +// const char* Worker::activeDOMObjectName() const +// { +// return "Worker"; +// } + +// void Worker::stop() +// { +// terminate(); +// } + +// void Worker::suspend(ReasonForSuspension reason) +// { +// if (reason == ReasonForSuspension::BackForwardCache) { +// m_contextProxy.suspendForBackForwardCache(); +// m_isSuspendedForBackForwardCache = true; +// } +// } + +// void Worker::resume() +// { +// if (m_isSuspendedForBackForwardCache) { +// m_contextProxy.resumeForBackForwardCache(); +// m_isSuspendedForBackForwardCache = false; +// } +// } + +bool Worker::hasPendingActivity() const +{ + if (this->m_isOnline) { + return !this->m_isClosing; + } + + return !this->m_wasTerminated; +} + +void Worker::dispatchEvent(Event& event) +{ + if (m_wasTerminated) + return; + + EventTarget::dispatchEvent(event); +} + +#if ENABLE(WEB_RTC) +void Worker::createRTCRtpScriptTransformer(RTCRtpScriptTransform& transform, MessageWithMessagePorts&& options) +{ + if (!scriptExecutionContext()) + return; + + m_contextProxy.postTaskToWorkerGlobalScope([transform = Ref { transform }, options = WTFMove(options)](auto& context) mutable { + if (auto transformer = downcast<DedicatedWorkerGlobalScope>(context).createRTCRtpScriptTransformer(WTFMove(options))) + transform->setTransformer(*transformer); + }); +} +#endif + +void Worker::drainEvents() +{ + for (auto& task : m_pendingTasks) + postTaskToWorkerGlobalScope(WTFMove(task)); + m_pendingTasks.clear(); +} + +void Worker::dispatchOnline(Zig::GlobalObject* workerGlobalObject) +{ + + auto* ctx = scriptExecutionContext(); + if (ctx) { + ScriptExecutionContext::postTaskTo(ctx->identifier(), [protectedThis = Ref { *this }](ScriptExecutionContext& context) -> void { + if (protectedThis->hasEventListeners(eventNames().openEvent)) { + auto event = Event::create(eventNames().openEvent, Event::CanBubble::No, Event::IsCancelable::No); + protectedThis->dispatchEvent(event); + } + }); + } + + this->m_isOnline = true; + auto* thisContext = workerGlobalObject->scriptExecutionContext(); + if (!thisContext) { + return; + } + + if (workerGlobalObject->eventTarget()->hasActiveEventListeners(eventNames().messageEvent)) { + auto tasks = std::exchange(this->m_pendingTasks, {}); + for (auto& task : tasks) { + task(*thisContext); + } + } else { + auto tasks = std::exchange(this->m_pendingTasks, {}); + thisContext->postTask([tasks = WTFMove(tasks)](auto& ctx) mutable { + for (auto& task : tasks) { + task(ctx); + } + tasks.clear(); + }); + } +} +void Worker::dispatchError(WTF::String message) +{ + + auto* ctx = scriptExecutionContext(); + if (!ctx) + return; + + ScriptExecutionContext::postTaskTo(ctx->identifier(), [protectedThis = Ref { *this }, message = message.isolatedCopy()](ScriptExecutionContext& context) -> void { + ErrorEvent::Init init; + init.message = message; + + auto event = ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes); + protectedThis->dispatchEvent(event); + }); +} +void Worker::dispatchExit() +{ + auto* ctx = scriptExecutionContext(); + if (!ctx) + return; + + ScriptExecutionContext::postTaskTo(ctx->identifier(), [protectedThis = Ref { *this }](ScriptExecutionContext& context) -> void { + protectedThis->m_isOnline = false; + protectedThis->m_isClosing = true; + protectedThis->setKeepAlive(false); + + if (protectedThis->hasEventListeners(eventNames().closeEvent)) { + auto event = Event::create(eventNames().closeEvent, Event::CanBubble::No, Event::IsCancelable::No); + protectedThis->dispatchEvent(event); + } + }); +} + +void Worker::postTaskToWorkerGlobalScope(Function<void(ScriptExecutionContext&)>&& task) +{ + if (!this->m_isOnline) { + this->m_pendingTasks.append(WTFMove(task)); + return; + } + + ScriptExecutionContext::postTaskTo(m_clientIdentifier, WTFMove(task)); +} + +void Worker::forEachWorker(const Function<Function<void(ScriptExecutionContext&)>()>& callback) +{ + Locker locker { allWorkersLock }; + for (auto& contextIdentifier : allWorkers().keys()) + ScriptExecutionContext::postTaskTo(contextIdentifier, callback()); +} + +extern "C" void WebWorker__dispatchExit(Zig::GlobalObject* globalObject, Worker* worker, int32_t exitCode) +{ + if (globalObject) { + auto& vm = globalObject->vm(); + + if (JSC::JSObject* obj = JSC::jsDynamicCast<JSC::JSObject*>(globalObject->moduleLoader())) { + auto id = JSC::Identifier::fromString(globalObject->vm(), "registry"_s); + if (auto* registry = JSC::jsDynamicCast<JSC::JSMap*>(obj->getIfPropertyExists(globalObject, id))) { + registry->clear(vm); + } + } + gcUnprotect(globalObject); + vm.deleteAllCode(JSC::DeleteAllCodeEffort::PreventCollectionAndDeleteAllCode); + vm.heap.reportAbandonedObjectGraph(); + WTF::releaseFastMallocFreeMemoryForThisThread(); + vm.notifyNeedTermination(); + vm.deferredWorkTimer->doWork(vm); + } + + worker->dispatchExit(); +} +extern "C" void WebWorker__dispatchOnline(Worker* worker, Zig::GlobalObject* globalObject) +{ + worker->dispatchOnline(globalObject); +} + +extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker* worker, BunString message, EncodedJSValue errorValue) +{ + JSValue error = JSC::JSValue::decode(errorValue); + ErrorEvent::Init init; + init.message = Bun::toWTFString(message).isolatedCopy(); + init.error = error; + init.cancelable = false; + init.bubbles = false; + + globalObject->eventTarget()->dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes)); + worker->dispatchError(Bun::toWTFString(message)); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/Worker.h b/src/bun.js/bindings/webcore/Worker.h new file mode 100644 index 000000000..edd9f082d --- /dev/null +++ b/src/bun.js/bindings/webcore/Worker.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008, 2010, 2016 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "ActiveDOMObject.h" +#include "EventTarget.h" +// #include "MessagePort.h" +#include "WorkerOptions.h" +// #include "WorkerScriptLoaderClient.h" +// #include "WorkerType.h" +#include <JavaScriptCore/RuntimeFlags.h> +#include <wtf/Deque.h> +#include <wtf/MonotonicTime.h> +#include <wtf/text/AtomStringHash.h> +#include "ContextDestructionObserver.h" +#include "Event.h" + +namespace JSC { +class CallFrame; +class JSObject; +class JSValue; +} + +namespace WebCore { + +class RTCRtpScriptTransform; +class RTCRtpScriptTransformer; +class ScriptExecutionContext; +class WorkerGlobalScopeProxy; +// class WorkerScriptLoader; + +struct StructuredSerializeOptions; +struct WorkerOptions; + +class Worker final : public RefCounted<Worker>, public EventTargetWithInlineData, private ContextDestructionObserver { + WTF_MAKE_ISO_ALLOCATED_EXPORT(Worker, WEBCORE_EXPORT); + +public: + static ExceptionOr<Ref<Worker>> create(ScriptExecutionContext&, const String& url, WorkerOptions&&); + ~Worker(); + + ExceptionOr<void> postMessage(JSC::JSGlobalObject&, JSC::JSValue message, StructuredSerializeOptions&&); + + using RefCounted::deref; + using RefCounted::ref; + + void terminate(); + bool wasTerminated() const { return m_wasTerminated; } + bool hasPendingActivity() const; + + String identifier() const { return m_identifier; } + const String& name() const { return m_options.name; } + + void dispatchEvent(Event&); + void setKeepAlive(bool); + +#if ENABLE(WEB_RTC) + void createRTCRtpScriptTransformer(RTCRtpScriptTransform&, MessageWithMessagePorts&&); +#endif + + // WorkerType type() const + // { + // return m_options.type; + // } + + void postTaskToWorkerGlobalScope(Function<void(ScriptExecutionContext&)>&&); + + static void forEachWorker(const Function<Function<void(ScriptExecutionContext&)>()>&); + + void drainEvents(); + void dispatchOnline(Zig::GlobalObject* workerGlobalObject); + void dispatchError(WTF::String message); + void dispatchExit(); + ScriptExecutionContext* scriptExecutionContext() const final { return ContextDestructionObserver::scriptExecutionContext(); } + +private: + Worker(ScriptExecutionContext&, WorkerOptions&&); + + EventTargetInterface eventTargetInterface() const final { return WorkerEventTargetInterfaceType; } + void refEventTarget() final { ref(); } + void derefEventTarget() final { deref(); } + void eventListenersDidChange() final {}; + + // void didReceiveResponse(ResourceLoaderIdentifier, const ResourceResponse&) final; + // void notifyFinished() final; + + // ActiveDOMObject. + // void stop() final; + // void suspend(ReasonForSuspension) final; + // void resume() final; + // const char* activeDOMObjectName() const final; + // bool virtualHasPendingActivity() const final; + + static void networkStateChanged(bool isOnLine); + + // RefPtr<WorkerScriptLoader> m_scriptLoader; + const WorkerOptions m_options; + String m_identifier; + // WorkerGlobalScopeProxy& m_contextProxy; // The proxy outlives the worker to perform thread shutdown. + // std::optional<ContentSecurityPolicyResponseHeaders> m_contentSecurityPolicyResponseHeaders; + MonotonicTime m_workerCreationTime; + // bool m_shouldBypassMainWorldContentSecurityPolicy { false }; + // bool m_isSuspendedForBackForwardCache { false }; + // JSC::RuntimeFlags m_runtimeFlags; + Deque<RefPtr<Event>> m_pendingEvents; + Deque<Function<void(ScriptExecutionContext&)>> m_pendingTasks; + bool m_wasTerminated { false }; + bool m_didStartWorkerGlobalScope { false }; + bool m_isOnline { false }; + bool m_isClosing { false }; + const ScriptExecutionContextIdentifier m_clientIdentifier; + void* impl_ { nullptr }; + size_t m_pendingActivityCount { 0 }; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/WorkerOptions.h b/src/bun.js/bindings/webcore/WorkerOptions.h new file mode 100644 index 000000000..bc2ef6ff0 --- /dev/null +++ b/src/bun.js/bindings/webcore/WorkerOptions.h @@ -0,0 +1,20 @@ +#pragma once + +#include "root.h" + +namespace WebCore { + +struct BunOptions { + bool mini { false }; + bool unref { false }; +}; + +struct WorkerOptions { + // WorkerType type { WorkerType::Classic }; + // FetchRequestCredentials credentials { FetchRequestCredentials::SameOrigin }; + String name; + + BunOptions bun {}; +}; + +} // namespace WebCore diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index a3ccd16ad..d9befef8e 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -746,14 +746,16 @@ pub const EventLoop = struct { this.virtual_machine.gc_controller.performGC(); } + pub fn wakeup(this: *EventLoop) void { + if (this.virtual_machine.uws_event_loop) |loop| { + loop.wakeup(); + } + } pub fn enqueueTaskConcurrent(this: *EventLoop, task: *ConcurrentTask) void { JSC.markBinding(@src()); this.concurrent_tasks.push(task); - - if (this.virtual_machine.uws_event_loop) |loop| { - loop.wakeup(); - } + this.wakeup(); } }; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index beb2d1856..3534a5316 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -96,7 +96,7 @@ pub const GlobalConstructors = [_]type{ pub const GlobalClasses = [_]type{ Bun.Class, WebCore.Crypto.Class, - EventListenerMixin.addEventListener(VirtualMachine), + // EventListenerMixin.addEventListener(VirtualMachine), // Fetch.Class, js_ast.Macro.JSNode.BunJSXCallbackFunction, @@ -353,7 +353,8 @@ pub const ExitHandler = struct { JSC.markBinding(@src()); var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); Process__dispatchOnExit(vm.global, this.exit_code); - Bun__closeAllSQLiteDatabasesForTermination(); + if (vm.isMainThread()) + Bun__closeAllSQLiteDatabasesForTermination(); } pub fn dispatchOnBeforeExit(this: *ExitHandler) void { @@ -363,6 +364,8 @@ pub const ExitHandler = struct { } }; +pub const WebWorker = @import("./web_worker.zig").WebWorker; + /// TODO: rename this to ScriptExecutionContext /// This is the shared global state for a single JS instance execution /// Today, Bun is one VM per thread, so the name "VirtualMachine" sort of makes sense @@ -482,11 +485,16 @@ pub const VirtualMachine = struct { parser_arena: ?@import("root").bun.ArenaAllocator = null, gc_controller: JSC.GarbageCollectionController = .{}, + worker: ?*JSC.WebWorker = null, pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void; pub const OnException = fn (*ZigException) void; + pub fn isMainThread(this: *const VirtualMachine) bool { + return this.worker == null; + } + pub fn setOnException(this: *VirtualMachine, callback: *const OnException) void { this.on_exception = callback; } @@ -877,6 +885,8 @@ pub const VirtualMachine = struct { &global_classes, @intCast(i32, global_classes.len), vm.console, + -1, + false, ); vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; @@ -976,6 +986,110 @@ pub const VirtualMachine = struct { &global_classes, @intCast(i32, global_classes.len), vm.console, + -1, + false, + ); + vm.regular_event_loop.global = vm.global; + vm.regular_event_loop.virtual_machine = vm; + + if (source_code_printer == null) { + var writer = try js_printer.BufferWriter.init(allocator); + source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + } + + return vm; + } + + pub fn initWorker( + allocator: std.mem.Allocator, + _args: Api.TransformOptions, + _log: ?*logger.Log, + env_loader: ?*DotEnv.Loader, + store_fd: bool, + worker: *WebWorker, + ) anyerror!*VirtualMachine { + var log: *logger.Log = undefined; + if (_log) |__log| { + log = __log; + } else { + log = try allocator.create(logger.Log); + log.* = logger.Log.init(allocator); + } + + VMHolder.vm = try allocator.create(VirtualMachine); + var console = try allocator.create(ZigConsoleClient); + console.* = ZigConsoleClient.init(Output.errorWriter(), Output.writer()); + const bundler = try Bundler.init( + allocator, + log, + try Config.configureTransformOptionsForBunVM(allocator, _args), + null, + env_loader, + ); + var vm = VMHolder.vm.?; + + vm.* = VirtualMachine{ + .global = undefined, + .allocator = allocator, + .entry_point = ServerEntryPoint{}, + .event_listeners = EventListenerMixin.Map.init(allocator), + .bundler = bundler, + .console = console, + .node_modules = bundler.options.node_modules_bundle, + .log = log, + .flush_list = std.ArrayList(string).init(allocator), + .blobs = if (_args.serve orelse false) try Blob.Group.init(allocator) else null, + .origin = bundler.options.origin, + .saved_source_map_table = SavedSourceMap.HashTable.init(allocator), + .source_mappings = undefined, + .macros = MacroMap.init(allocator), + .macro_entry_points = @TypeOf(vm.macro_entry_points).init(allocator), + .origin_timer = std.time.Timer.start() catch @panic("Please don't mess with timers."), + .origin_timestamp = getOriginTimestamp(), + .ref_strings = JSC.RefString.Map.init(allocator), + .file_blobs = JSC.WebCore.Blob.Store.Map.init(allocator), + .parser_arena = @import("root").bun.ArenaAllocator.init(allocator), + .standalone_module_graph = worker.parent.standalone_module_graph, + .worker = worker, + }; + vm.source_mappings = .{ .map = &vm.saved_source_map_table }; + vm.regular_event_loop.tasks = EventLoop.Queue.init( + default_allocator, + ); + vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; + vm.regular_event_loop.concurrent_tasks = .{}; + vm.event_loop = &vm.regular_event_loop; + vm.hot_reload = worker.parent.hot_reload; + vm.bundler.macro_context = null; + vm.bundler.resolver.store_fd = store_fd; + vm.bundler.resolver.prefer_module_field = false; + vm.bundler.resolver.onWakePackageManager = .{ + .context = &vm.modules, + .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, + .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, + }; + + vm.bundler.configureLinker(); + try vm.bundler.configureFramework(false); + + vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + + if (_args.serve orelse false) { + vm.bundler.linker.onImportCSS = Bun.onImportCSS; + } + + var global_classes: [GlobalClasses.len]js.JSClassRef = undefined; + inline for (GlobalClasses, 0..) |Class, i| { + global_classes[i] = Class.get().*; + } + vm.global = ZigGlobalObject.create( + &global_classes, + @intCast(i32, global_classes.len), + vm.console, + @intCast(i32, worker.execution_context_id), + worker.mini, ); vm.regular_event_loop.global = vm.global; vm.regular_event_loop.virtual_machine = vm; @@ -1753,7 +1867,7 @@ pub const VirtualMachine = struct { return promise; } - pub fn loadEntryPoint(this: *VirtualMachine, entry_path: string) !*JSInternalPromise { + pub fn loadEntryPoint(this: *VirtualMachine, entry_path: string) anyerror!*JSInternalPromise { var promise = try this.reloadEntryPoint(entry_path); // pending_internal_promise can change if hot module reloading is enabled diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 64bd04e94..e035c01a6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -2223,8 +2223,14 @@ pub const Process = struct { } pub fn exit(globalObject: *JSC.JSGlobalObject, code: i32) callconv(.C) void { - globalObject.bunVM().onExit(); + var vm = globalObject.bunVM(); + if (vm.worker) |worker| { + vm.exit_handler.exit_code = @truncate(u8, @max(code, 0)); + worker.terminate(); + return; + } + vm.onExit(); std.os.exit(@truncate(u8, @intCast(u32, @max(code, 0)))); } diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig new file mode 100644 index 000000000..df2e63c3a --- /dev/null +++ b/src/bun.js/web_worker.zig @@ -0,0 +1,406 @@ +const bun = @import("root").bun; +const JSC = bun.JSC; +const Output = bun.Output; +const log = Output.scoped(.Worker, true); +const std = @import("std"); +const JSValue = JSC.JSValue; + +pub const WebWorker = struct { + // null when haven't started yet + vm: ?*JSC.VirtualMachine = null, + status: Status = .start, + requested_terminate: bool = false, + execution_context_id: u32 = 0, + parent_context_id: u32 = 0, + parent: *JSC.VirtualMachine, + + /// Already resolved. + specifier: []const u8 = "", + store_fd: bool = false, + arena: bun.MimallocArena = undefined, + name: [:0]const u8 = "Worker", + cpp_worker: *void, + allowed_to_exit: bool = false, + mini: bool = false, + parent_poll_ref: JSC.PollRef = .{}, + initial_poll_ref: JSC.PollRef = .{}, + + extern fn WebWorker__dispatchExit(?*JSC.JSGlobalObject, *void, i32) void; + extern fn WebWorker__dispatchOnline(this: *void, *JSC.JSGlobalObject) void; + extern fn WebWorker__dispatchError(*JSC.JSGlobalObject, *void, bun.String, JSValue) void; + export fn WebWorker__getParentWorker(vm: *JSC.VirtualMachine) ?*anyopaque { + var worker = vm.worker orelse return null; + return worker.cpp_worker; + } + + pub fn hasPendingActivity(this: *WebWorker) callconv(.C) bool { + JSC.markBinding(@src()); + + if (this.vm == null) { + return !this.requested_terminate; + } + + if (!this.allowed_to_exit) { + return true; + } + + var vm = this.vm.?; + + return vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or vm.uws_event_loop.?.active > 0; + } + + pub fn create( + cpp_worker: *void, + parent: *JSC.VirtualMachine, + name_str: bun.String, + specifier_str: bun.String, + error_message: *bun.String, + parent_context_id: u32, + this_context_id: u32, + mini: bool, + default_unref: bool, + ) callconv(.C) ?*WebWorker { + JSC.markBinding(@src()); + var spec_slice = specifier_str.toUTF8(bun.default_allocator); + defer spec_slice.deinit(); + var prev_log = parent.bundler.log; + var temp_log = bun.logger.Log.init(bun.default_allocator); + parent.bundler.setLog(&temp_log); + defer parent.bundler.setLog(prev_log); + defer temp_log.deinit(); + + var resolved_entry_point = parent.bundler.resolveEntryPoint(spec_slice.slice()) catch { + var out = temp_log.toJS(parent.global, bun.default_allocator, "Error resolving Worker entry point").toBunString(parent.global); + out.ref(); + error_message.* = out; + return null; + }; + + var path = resolved_entry_point.path() orelse { + error_message.* = bun.String.static("Worker entry point is missing"); + return null; + }; + + var worker = bun.default_allocator.create(WebWorker) catch @panic("OOM"); + worker.* = WebWorker{ + .cpp_worker = cpp_worker, + .parent = parent, + .parent_context_id = parent_context_id, + .execution_context_id = this_context_id, + .mini = mini, + .specifier = bun.default_allocator.dupe(u8, path.text) catch @panic("OOM"), + .store_fd = parent.bundler.resolver.store_fd, + .name = brk: { + if (!name_str.isEmpty()) { + break :brk std.fmt.allocPrintZ(bun.default_allocator, "{}", .{name_str}) catch @panic("OOM"); + } + break :brk ""; + }, + }; + + worker.initial_poll_ref.ref(parent); + + if (!default_unref) { + worker.allowed_to_exit = false; + worker.parent_poll_ref.ref(parent); + } + + var thread = std.Thread.spawn( + .{ .stack_size = 2 * 1024 * 1024 }, + startWithErrorHandling, + .{worker}, + ) catch { + worker.deinit(); + worker.requested_terminate = true; + worker.parent_poll_ref.unref(worker.parent); + worker.initial_poll_ref.unref(worker.parent); + return null; + }; + thread.detach(); + + return worker; + } + + fn queueInitialTask(this: *WebWorker) void { + const Unref = struct { + pub fn unref(worker: *WebWorker) void { + worker.initial_poll_ref.unref(worker.parent); + } + }; + + const AnyTask = JSC.AnyTask.New(WebWorker, Unref.unref); + var any_task = bun.default_allocator.create(JSC.AnyTask) catch @panic("OOM"); + any_task.* = AnyTask.init(this); + var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch @panic("OOM"); + this.parent.eventLoop().enqueueTaskConcurrent(concurrent_task.from(any_task)); + } + + pub fn startWithErrorHandling( + this: *WebWorker, + ) void { + start(this) catch |err| { + Output.panic("An unhandled error occurred while starting a worker: {s}\n", .{@errorName(err)}); + }; + } + + pub fn start( + this: *WebWorker, + ) anyerror!void { + if (this.name.len > 0) { + Output.Source.configureNamedThread(this.name); + } else { + Output.Source.configureNamedThread("Worker"); + } + + if (this.requested_terminate) { + this.queueInitialTask(); + this.deinit(); + return; + } + + std.debug.assert(this.status == .start); + std.debug.assert(this.vm == null); + this.arena = try bun.MimallocArena.init(); + var vm = try JSC.VirtualMachine.initWorker( + this.arena.allocator(), + this.parent.bundler.options.transform_options, + null, + null, + this.store_fd, + this, + ); + vm.allocator = this.arena.allocator(); + vm.arena = &this.arena; + + var b = &vm.bundler; + + b.configureRouter(false) catch { + this.flushLogs(); + this.onTerminate(); + return; + }; + b.configureDefines() catch { + this.flushLogs(); + this.onTerminate(); + return; + }; + + vm.loadExtraEnv(); + vm.is_main_thread = false; + JSC.VirtualMachine.is_main_thread_vm = false; + vm.onUnhandledRejection = onUnhandledRejection; + var callback = JSC.OpaqueWrap(WebWorker, WebWorker.spin); + + this.vm = vm; + + vm.global.vm().holdAPILock(this, callback); + } + + fn deinit(this: *WebWorker) void { + bun.default_allocator.free(this.specifier); + } + + fn flushLogs(this: *WebWorker) void { + var vm = this.vm orelse return; + if (vm.log.msgs.items.len == 0) return; + const err = vm.log.toJS(vm.global, bun.default_allocator, "Error in worker"); + const str = err.toBunString(vm.global); + WebWorker__dispatchError(vm.global, this.cpp_worker, str, err); + } + + fn onUnhandledRejection(vm: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, error_instance: JSC.JSValue) void { + var array = bun.MutableString.init(bun.default_allocator, 0) catch unreachable; + defer array.deinit(); + + var buffered_writer_ = bun.MutableString.BufferedWriter{ .context = &array }; + var buffered_writer = &buffered_writer_; + var worker = vm.worker orelse @panic("Assertion failure: no worker"); + + var writer = buffered_writer.writer(); + const Writer = @TypeOf(writer); + // we buffer this because it'll almost always be < 4096 + // when it's under 4096, we want to avoid the dynamic allocation + bun.JSC.ZigConsoleClient.format( + .Debug, + globalObject, + &[_]JSC.JSValue{error_instance}, + 1, + Writer, + Writer, + writer, + .{ + .enable_colors = false, + .add_newline = false, + .flush = false, + .max_depth = 32, + }, + ); + buffered_writer.flush() catch { + @panic("OOM"); + }; + + WebWorker__dispatchError(globalObject, worker.cpp_worker, bun.String.create(array.toOwnedSliceLeaky()), error_instance); + } + + fn setStatus(this: *WebWorker, status: Status) void { + log("status: {s}", .{@tagName(status)}); + this.status = status; + } + + fn unhandledError(this: *WebWorker, _: anyerror) void { + this.flushLogs(); + } + + fn spin(this: *WebWorker) void { + var vm = this.vm.?; + std.debug.assert(this.status == .start); + this.setStatus(.starting); + + var promise = vm.loadEntryPoint(this.specifier) catch { + this.flushLogs(); + this.onTerminate(); + return; + }; + + this.queueInitialTask(); + + if (promise.status(vm.global.vm()) == .Rejected) { + vm.onUnhandledError(vm.global, promise.result(vm.global.vm())); + + vm.exit_handler.exit_code = 1; + this.onTerminate(); + + return; + } + + _ = promise.result(vm.global.vm()); + + this.flushLogs(); + + WebWorker__dispatchOnline(this.cpp_worker, vm.global); + this.setStatus(.running); + + // don't run the GC if we don't actually need to + if (vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or + vm.uws_event_loop.?.active > 0 or + vm.eventLoop().tickConcurrentWithCount() > 0) + { + vm.global.vm().releaseWeakRefs(); + _ = vm.arena.gc(false); + _ = vm.global.vm().runGC(false); + vm.tick(); + } + + { + while (true) { + while (vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or vm.uws_event_loop.?.active > 0) { + vm.tick(); + + vm.eventLoop().autoTickActive(); + } + + if (!this.allowed_to_exit) { + this.flushLogs(); + vm.eventLoop().tickPossiblyForever(); + continue; + } + + vm.onBeforeExit(); + + if (!this.allowed_to_exit) + continue; + + break; + } + + this.flushLogs(); + this.onTerminate(); + } + } + + pub const Status = enum { + start, + starting, + running, + terminated, + }; + + pub fn terminate(this: *WebWorker) callconv(.C) void { + if (this.requested_terminate) { + return; + } + + this.allowed_to_exit = false; + _ = this.requestTerminate(); + } + + pub fn setRef(this: *WebWorker, value: bool) callconv(.C) void { + if (this.requested_terminate and value) { + this.parent_poll_ref.unref(this.parent); + return; + } + + this.allowed_to_exit = value; + if (value) { + this.parent_poll_ref.ref(this.parent); + } else { + this.parent_poll_ref.unref(this.parent); + } + + if (this.vm) |vm| { + vm.eventLoop().wakeup(); + } + } + + fn onTerminate(this: *WebWorker) void { + log("onTerminate", .{}); + + this.reallyExit(); + } + + comptime { + if (!JSC.is_bindgen) { + @export(hasPendingActivity, .{ .name = "WebWorker__hasPendingActivity" }); + @export(create, .{ .name = "WebWorker__create" }); + @export(terminate, .{ .name = "WebWorker__terminate" }); + @export(setRef, .{ .name = "WebWorker__setRef" }); + } + } + + fn reallyExit(this: *WebWorker) void { + JSC.markBinding(@src()); + + if (this.requested_terminate) { + return; + } + this.requested_terminate = true; + + var cpp_worker = this.cpp_worker; + var exit_code: i32 = 0; + var globalObject: ?*JSC.JSGlobalObject = null; + if (this.vm) |vm| { + this.vm = null; + vm.onExit(); + exit_code = vm.exit_handler.exit_code; + globalObject = vm.global; + this.arena.deinit(); + } + WebWorker__dispatchExit(globalObject, cpp_worker, exit_code); + + this.deinit(); + } + + fn requestTerminate(this: *WebWorker) bool { + var vm = this.vm orelse { + this.requested_terminate = true; + return false; + }; + this.allowed_to_exit = true; + log("requesting terminate", .{}); + var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch @panic("OOM"); + var task = bun.default_allocator.create(JSC.AnyTask) catch @panic("OOM"); + task.* = JSC.AnyTask.New(WebWorker, onTerminate).init(this); + vm.eventLoop().enqueueTaskConcurrent(concurrent_task.from(task)); + return true; + } +}; diff --git a/src/js/bun/jsc.ts b/src/js/bun/jsc.ts index 154e7345c..8063da948 100644 --- a/src/js/bun/jsc.ts +++ b/src/js/bun/jsc.ts @@ -34,3 +34,5 @@ export const profile = jsc.profile; export default jsc; export const setTimeZone = jsc.setTimeZone; export const setTimezone = setTimeZone; +export const serialize = jsc.serialize; +export const deserialize = jsc.deserialize; diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts index 3018c34e6..b043e10fe 100644 --- a/src/js/node/v8.ts +++ b/src/js/node/v8.ts @@ -1,6 +1,7 @@ // Hardcoded module "node:v8" // This is a stub! None of this is actually implemented yet. import { hideFromStack, throwNotImplemented } from "../shared"; +import { serialize as jscSerialize, deserialize as jscDeserialize } from "bun:jsc"; function notimpl(message) { throwNotImplemented("node:v8 " + message); @@ -42,8 +43,8 @@ function getHeapCodeStatistics() { function setFlagsFromString() { notimpl("setFlagsFromString"); } -function deserialize() { - notimpl("deserialize"); +function deserialize(value) { + return jscDeserialize(value); } function takeCoverage() { notimpl("takeCoverage"); @@ -51,8 +52,8 @@ function takeCoverage() { function stopCoverage() { notimpl("stopCoverage"); } -function serialize() { - notimpl("serialize"); +function serialize(arg1) { + return jscSerialize(arg1, { binaryType: "nodebuffer" }); } function writeHeapSnapshot() { notimpl("writeHeapSnapshot"); diff --git a/src/js/out/modules/bun/jsc.js b/src/js/out/modules/bun/jsc.js index df89b2584..45fb51bdd 100644 --- a/src/js/out/modules/bun/jsc.js +++ b/src/js/out/modules/bun/jsc.js @@ -1,4 +1,4 @@ -var jsc = globalThis[Symbol.for("Bun.lazy")]("bun:jsc"), callerSourceOrigin = jsc.callerSourceOrigin, jscDescribe = jsc.describe, jscDescribeArray = jsc.describeArray, describe = jscDescribe, describeArray = jscDescribeArray, drainMicrotasks = jsc.drainMicrotasks, edenGC = jsc.edenGC, fullGC = jsc.fullGC, gcAndSweep = jsc.gcAndSweep, getRandomSeed = jsc.getRandomSeed, heapSize = jsc.heapSize, heapStats = jsc.heapStats, startSamplingProfiler = jsc.startSamplingProfiler, samplingProfilerStackTraces = jsc.samplingProfilerStackTraces, isRope = jsc.isRope, memoryUsage = jsc.memoryUsage, noInline = jsc.noInline, noFTL = jsc.noFTL, noOSRExitFuzzing = jsc.noOSRExitFuzzing, numberOfDFGCompiles = jsc.numberOfDFGCompiles, optimizeNextInvocation = jsc.optimizeNextInvocation, releaseWeakRefs = jsc.releaseWeakRefs, reoptimizationRetryCount = jsc.reoptimizationRetryCount, setRandomSeed = jsc.setRandomSeed, startRemoteDebugger = jsc.startRemoteDebugger, totalCompileTime = jsc.totalCompileTime, getProtectedObjects = jsc.getProtectedObjects, generateHeapSnapshotForDebugging = jsc.generateHeapSnapshotForDebugging, profile = jsc.profile, jsc_default = jsc, setTimeZone = jsc.setTimeZone, setTimezone = setTimeZone; +var jsc = globalThis[Symbol.for("Bun.lazy")]("bun:jsc"), callerSourceOrigin = jsc.callerSourceOrigin, jscDescribe = jsc.describe, jscDescribeArray = jsc.describeArray, describe = jscDescribe, describeArray = jscDescribeArray, drainMicrotasks = jsc.drainMicrotasks, edenGC = jsc.edenGC, fullGC = jsc.fullGC, gcAndSweep = jsc.gcAndSweep, getRandomSeed = jsc.getRandomSeed, heapSize = jsc.heapSize, heapStats = jsc.heapStats, startSamplingProfiler = jsc.startSamplingProfiler, samplingProfilerStackTraces = jsc.samplingProfilerStackTraces, isRope = jsc.isRope, memoryUsage = jsc.memoryUsage, noInline = jsc.noInline, noFTL = jsc.noFTL, noOSRExitFuzzing = jsc.noOSRExitFuzzing, numberOfDFGCompiles = jsc.numberOfDFGCompiles, optimizeNextInvocation = jsc.optimizeNextInvocation, releaseWeakRefs = jsc.releaseWeakRefs, reoptimizationRetryCount = jsc.reoptimizationRetryCount, setRandomSeed = jsc.setRandomSeed, startRemoteDebugger = jsc.startRemoteDebugger, totalCompileTime = jsc.totalCompileTime, getProtectedObjects = jsc.getProtectedObjects, generateHeapSnapshotForDebugging = jsc.generateHeapSnapshotForDebugging, profile = jsc.profile, jsc_default = jsc, setTimeZone = jsc.setTimeZone, setTimezone = setTimeZone, serialize = jsc.serialize, deserialize = jsc.deserialize; export { totalCompileTime, startSamplingProfiler, @@ -6,6 +6,7 @@ export { setTimezone, setTimeZone, setRandomSeed, + serialize, samplingProfilerStackTraces, reoptimizationRetryCount, releaseWeakRefs, @@ -28,6 +29,7 @@ export { fullGC, edenGC, drainMicrotasks, + deserialize, describeArray, describe, jsc_default as default, diff --git a/src/js/out/modules/node/v8.js b/src/js/out/modules/node/v8.js index eb7a6568b..b2a42e898 100644 --- a/src/js/out/modules/node/v8.js +++ b/src/js/out/modules/node/v8.js @@ -17,6 +17,7 @@ class NotImplementedError extends Error { } // src/js/node/v8.ts +import {serialize as jscSerialize, deserialize as jscDeserialize} from "bun:jsc"; var notimpl = function(message) { throwNotImplemented("node:v8 " + message); }, cachedDataVersionTag = function() { @@ -31,14 +32,14 @@ var notimpl = function(message) { notimpl("getHeapCodeStatistics"); }, setFlagsFromString = function() { notimpl("setFlagsFromString"); -}, deserialize = function() { - notimpl("deserialize"); +}, deserialize = function(value) { + return jscDeserialize(value); }, takeCoverage = function() { notimpl("takeCoverage"); }, stopCoverage = function() { notimpl("stopCoverage"); -}, serialize = function() { - notimpl("serialize"); +}, serialize = function(arg1) { + return jscSerialize(arg1, { binaryType: "nodebuffer" }); }, writeHeapSnapshot = function() { notimpl("writeHeapSnapshot"); }, setHeapSnapshotNearHeapLimit = function() { diff --git a/test/js/bun/jsc/bun-jsc.test.ts b/test/js/bun/jsc/bun-jsc.test.ts index e8aa4229f..e993dd723 100644 --- a/test/js/bun/jsc/bun-jsc.test.ts +++ b/test/js/bun/jsc/bun-jsc.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "bun:test"; import { describe as jscDescribe, describeArray, + serialize, + deserialize, gcAndSweep, fullGC, edenGC, @@ -140,4 +142,28 @@ describe("bun:jsc", () => { expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(origTimezone); }); + + it("serialize", () => { + const serialized = serialize({ a: 1 }); + expect(serialized).toBeInstanceOf(SharedArrayBuffer); + expect(deserialize(serialized)).toStrictEqual({ a: 1 }); + const nested = serialize(serialized); + expect(deserialize(deserialize(nested))).toStrictEqual({ a: 1 }); + }); + + it("serialize (binaryType: 'nodebuffer')", () => { + const serialized = serialize({ a: 1 }, { binaryType: "nodebuffer" }); + expect(serialized).toBeInstanceOf(Buffer); + expect(serialized.buffer).toBeInstanceOf(SharedArrayBuffer); + expect(deserialize(serialized)).toStrictEqual({ a: 1 }); + const nested = serialize(serialized); + expect(deserialize(deserialize(nested))).toStrictEqual({ a: 1 }); + }); + + it("serialize GC test", () => { + for (let i = 0; i < 1000; i++) { + serialize({ a: 1 }); + } + Bun.gc(true); + }); }); diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index d687a1290..46422c210 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -19,8 +19,59 @@ test("exists", () => { expect(typeof WebSocket !== "undefined").toBe(true); expect(typeof Blob !== "undefined").toBe(true); expect(typeof FormData !== "undefined").toBe(true); + expect(typeof Worker !== "undefined").toBe(true); }); +const globalSetters = [ + [ErrorEvent, "onerror", "error", "error"], + [MessageEvent, "onmessage", "message", "data"], +]; + +for (const [Constructor, name, eventName, prop] of globalSetters) { + test(`self.${name}`, () => { + var called = false; + + const callback = ({ [prop]: data }) => { + expect(data).toBe("hello"); + called = true; + }; + + try { + globalThis[name] = callback; + expect(globalThis[name]).toBe(callback); + dispatchEvent(new Constructor(eventName, { data: "hello", error: "hello" })); + expect(called).toBe(true); + } finally { + globalThis[name] = null; + + called = false; + dispatchEvent(new Constructor(eventName, { data: "hello", error: "hello" })); + expect(called).toBe(false); + } + }); + + test(`self.addEventListener(${name})`, () => { + var called = false; + + const callback = ({ [prop]: data }) => { + expect(data).toBe("hello"); + called = true; + }; + + try { + addEventListener(eventName, callback); + dispatchEvent(new Constructor(eventName, { data: "hello", error: "hello" })); + expect(called).toBe(true); + } finally { + globalThis[name] = null; + removeEventListener(eventName, callback); + called = false; + dispatchEvent(new Constructor(eventName, { data: "hello", error: "hello" })); + expect(called).toBe(false); + } + }); +} + test("CloseEvent", () => { var event = new CloseEvent("close", { reason: "world" }); expect(event.type).toBe("close"); diff --git a/test/js/web/worker-fixture.js b/test/js/web/worker-fixture.js new file mode 100644 index 000000000..9e9dcad0a --- /dev/null +++ b/test/js/web/worker-fixture.js @@ -0,0 +1,6 @@ +postMessage("initial message"); +onmessage = ({ data }) => { + postMessage({ + received: data, + }); +}; diff --git a/test/js/web/worker.test.ts b/test/js/web/worker.test.ts new file mode 100644 index 000000000..2120bd11a --- /dev/null +++ b/test/js/web/worker.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test"; + +test("worker", done => { + const worker = new Worker(new URL("worker-fixture.js", import.meta.url).href, { + bun: { + smol: true, + }, + }); + worker.postMessage("hello"); + worker.onerror = e => { + done(e.error); + }; + worker.onmessage = e => { + expect(e.data).toEqual("initial message"); + worker.terminate(); + done(); + }; +}); |