From 2a2247bbb688364c95a7dc390cebf97f87b2b6f1 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 29 Apr 2025 17:23:26 -0800 Subject: [PATCH] js: fix serialization of non-transferable objects (#19351) --- src/bun.js/bindings/JSValue.zig | 6 +- src/bun.js/bindings/Serialization.cpp | 8 ++- src/bun.js/bindings/ZigGlobalObject.cpp | 3 +- .../webcore/SerializedScriptValue.cpp | 34 +++++---- .../bindings/webcore/SerializedScriptValue.h | 9 ++- src/bun.js/ipc.zig | 6 +- src/bun.js/modules/BunJSCModule.h | 9 +-- src/codegen/class-definitions.ts | 2 +- src/codegen/generate-classes.ts | 32 +++++++-- test/harness.ts | 2 +- test/js/node/cluster.test.ts | 70 +++++++++++++++++++ test/js/web/workers/message-channel.test.ts | 47 +++++++++++++ 12 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 test/js/node/cluster.test.ts diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index d13ab88e1e..bf35de3cdf 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -2591,7 +2591,7 @@ pub const JSValue = enum(i64) { return Bun__JSValue__deserialize(global, bytes.ptr, bytes.len); } - extern fn Bun__serializeJSValue(global: *JSC.JSGlobalObject, value: JSValue) SerializedScriptValue.External; + extern fn Bun__serializeJSValue(global: *JSC.JSGlobalObject, value: JSValue, forTransfer: bool) SerializedScriptValue.External; extern fn Bun__SerializedScriptSlice__free(*anyopaque) void; pub const SerializedScriptValue = struct { @@ -2611,8 +2611,8 @@ pub const JSValue = enum(i64) { /// Throws a JS exception and returns null if the serialization fails, otherwise returns a SerializedScriptValue. /// Must be freed when you are done with the bytes. - pub inline fn serialize(this: JSValue, global: *JSGlobalObject) ?SerializedScriptValue { - const value = Bun__serializeJSValue(global, this); + pub inline fn serialize(this: JSValue, global: *JSGlobalObject, forTransfer: bool) ?SerializedScriptValue { + const value = Bun__serializeJSValue(global, this, forTransfer); return if (value.bytes) |bytes| .{ .data = bytes[0..value.size], .handle = value.handle.? } else diff --git a/src/bun.js/bindings/Serialization.cpp b/src/bun.js/bindings/Serialization.cpp index f48b8bf35c..ffa72ea3b3 100644 --- a/src/bun.js/bindings/Serialization.cpp +++ b/src/bun.js/bindings/Serialization.cpp @@ -16,14 +16,16 @@ struct SerializedValueSlice { }; /// Returns a "slice" that also contains a pointer to the SerializedScriptValue. Must be freed by the caller -extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObject, EncodedJSValue encodedValue) +extern "C" SerializedValueSlice Bun__serializeJSValue(JSGlobalObject* globalObject, EncodedJSValue encodedValue, bool forTransferBool) { JSValue value = JSValue::decode(encodedValue); Vector> transferList; Vector> dummyPorts; - ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), - dummyPorts); + auto forStorage = SerializationForStorage::No; + auto context = SerializationContext::Default; + auto forTransferEnum = forTransferBool ? SerializationForTransfer::Yes : SerializationForTransfer::No; + ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts, forStorage, context, forTransferEnum); auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 5d305fae6f..0a5da5acc2 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1524,8 +1524,7 @@ JSC_DEFINE_HOST_FUNCTION(functionNativeMicrotaskTrampoline, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(functionStructuredClone, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +JSC_DEFINE_HOST_FUNCTION(functionStructuredClone, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 3d62140d0c..6c4a4291eb 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -887,7 +887,7 @@ public: WasmMemoryHandleArray& wasmMemoryHandles, #endif Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, - SerializationForStorage forStorage) + SerializationForStorage forStorage, SerializationForTransfer forTransfer) { CloneSerializer serializer(lexicalGlobalObject, messagePorts, arrayBuffers, #if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS) @@ -904,7 +904,7 @@ public: wasmModules, wasmMemoryHandles, #endif - out, context, sharedBuffers, forStorage); + out, context, sharedBuffers, forStorage, forTransfer); return serializer.serialize(value); } @@ -989,7 +989,7 @@ private: WasmModuleArray& wasmModules, WasmMemoryHandleArray& wasmMemoryHandles, #endif - Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, SerializationForStorage forStorage) + Vector& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, SerializationForStorage forStorage, SerializationForTransfer forTransfer) : CloneBase(lexicalGlobalObject) , m_buffer(out) , m_emptyIdentifier(Identifier::fromString(lexicalGlobalObject->vm(), emptyString())) @@ -1004,6 +1004,7 @@ private: , m_serializedVideoFrames(serializedVideoFrames) #endif , m_forStorage(forStorage) + , m_forTransfer(forTransfer) { write(CurrentVersion); fillTransferMap(messagePorts, m_transferredMessagePorts); @@ -1829,7 +1830,7 @@ private: dummyModules, dummyMemoryHandles, #endif - serializedKey, SerializationContext::Default, dummySharedBuffers, m_forStorage); + serializedKey, SerializationContext::Default, dummySharedBuffers, m_forStorage, m_forTransfer); rawKeySerializer.write(key); Vector wrappedKey; @@ -1943,6 +1944,11 @@ private: // write bun types if (auto _cloneable = StructuredCloneableSerialize::fromJS(value)) { + if (m_forTransfer == SerializationForTransfer::Yes && !SerializedScriptValue::isTransferable(m_lexicalGlobalObject, value)) { + write(ObjectTag); + write(TerminatorTag); + return true; + } StructuredCloneableSerialize cloneable = WTFMove(_cloneable.value()); write(cloneable.tag); cloneable.write(this, m_lexicalGlobalObject); @@ -2526,6 +2532,7 @@ private: Vector>& m_serializedVideoFrames; #endif SerializationForStorage m_forStorage; + SerializationForTransfer m_forTransfer; }; SYSV_ABI void SerializedScriptValue::writeBytesForBun(CloneSerializer* ctx, const uint8_t* data, uint32_t size) @@ -5681,10 +5688,10 @@ static bool canDetachRTCDataChannels(const Vector>& channels } #endif -RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext) +RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext, SerializationForTransfer forTransfer) { Vector> dummyPorts; - auto result = create(globalObject, value, {}, dummyPorts, forStorage, throwExceptions, serializationContext); + auto result = create(globalObject, value, {}, dummyPorts, forStorage, throwExceptions, serializationContext, forTransfer); // auto result = create(globalObject, value, {}, forStorage, throwExceptions, serializationContext); if (result.hasException()) return nullptr; @@ -5696,13 +5703,13 @@ RefPtr SerializedScriptValue::create(JSC::JSGlobalObject& // return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::NonThrowing, serializationContext); // } -ExceptionOr> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationContext serializationContext) +ExceptionOr> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationContext serializationContext, SerializationForTransfer forTransfer) { - return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::Throwing, serializationContext); + return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::Throwing, serializationContext, forTransfer); } // ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context) -ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context) +ExceptionOr> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector>&& transferList, Vector>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context, SerializationForTransfer forTransfer) { VM& vm = lexicalGlobalObject.vm(); Vector> arrayBuffers; @@ -5828,7 +5835,7 @@ ExceptionOr> SerializedScriptValue::create(JSGlobalOb wasmModules, wasmMemoryHandles, #endif - buffer, context, *sharedBuffers, forStorage); + buffer, context, *sharedBuffers, forStorage, forTransfer); // Serialize may throw an exception. This code looks weird, but we'll rethrow it // in maybeThrowExceptionIfSerializationFailed (since that may also throw other @@ -5843,8 +5850,9 @@ ExceptionOr> SerializedScriptValue::create(JSGlobalOb return exceptionForSerializationFailure(code); auto arrayBufferContentsArray = transferArrayBuffers(vm, arrayBuffers); - if (arrayBufferContentsArray.hasException()) + if (arrayBufferContentsArray.hasException()) { return arrayBufferContentsArray.releaseException(); + } // auto backingStores = ImageBitmap::detachBitmaps(WTFMove(imageBitmaps)); @@ -6056,9 +6064,9 @@ JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject, maybeThrowExceptionIfSerializationFailed(lexicalGlobalObject, result.second); // Rethrow is a bit simpler here since we don't deal with return codes. - RETURN_IF_EXCEPTION(scope, jsNull()); + RETURN_IF_EXCEPTION(scope, {}); - return result.first ? result.first : jsNull(); + return result.first; } // JSValue SerializedScriptValue::deserialize(JSGlobalObject& lexicalGlobalObject, JSGlobalObject* globalObject, const Vector& blobURLs, const Vector& blobFilePaths, SerializationErrorMode throwExceptions, bool* didFail) // { diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.h b/src/bun.js/bindings/webcore/SerializedScriptValue.h index f000d6ea6a..f3ed0f33d3 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.h +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.h @@ -75,6 +75,8 @@ enum class SerializationContext { Default, WindowPostMessage }; enum class SerializationForStorage : bool { No, Yes }; +enum class SerializationForTransfer : bool { No, + Yes }; using ArrayBufferContentsArray = Vector; #if ENABLE(WEBASSEMBLY) @@ -88,11 +90,12 @@ class SerializedScriptValue : public ThreadSafeRefCounted public: static SYSV_ABI void writeBytesForBun(CloneSerializer*, const uint8_t*, uint32_t); + static SYSV_ABI bool isTransferable(JSC::JSGlobalObject* globalObject, JSC::JSValue value); - WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default); + WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No); // WEBCORE_EXPORT static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default); - WEBCORE_EXPORT static RefPtr create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default); + WEBCORE_EXPORT static RefPtr create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No); static RefPtr convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { return create(globalObject, value, SerializationForStorage::Yes); } @@ -148,7 +151,7 @@ private: // Vector>&& = {}, Vector&& = {} // #endif // ); - static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage, SerializationErrorMode, SerializationContext); + static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, Vector>&& transfer, Vector>&, SerializationForStorage, SerializationErrorMode, SerializationContext, SerializationForTransfer); WEBCORE_EXPORT SerializedScriptValue(Vector&&, std::unique_ptr&& = nullptr #if ENABLE(WEB_RTC) , diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 5d2e5d5fd5..1c263afa22 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -144,8 +144,7 @@ const advanced = struct { } pub fn serialize(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - const serialized = value.serialize(global) orelse - return IPCSerializationError.SerializationFailed; + const serialized = value.serialize(global, true) orelse return IPCSerializationError.SerializationFailed; defer serialized.deinit(); const size: u32 = @intCast(serialized.data.len); @@ -162,8 +161,7 @@ const advanced = struct { } pub fn serializeInternal(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - const serialized = value.serialize(global) orelse - return IPCSerializationError.SerializationFailed; + const serialized = value.serialize(global, true) orelse return IPCSerializationError.SerializationFailed; defer serialized.deinit(); const size: u32 = @intCast(serialized.data.len); diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h index 2f21bf5f46..96474c4496 100644 --- a/src/bun.js/modules/BunJSCModule.h +++ b/src/bun.js/modules/BunJSCModule.h @@ -767,11 +767,9 @@ JSC_DEFINE_HOST_FUNCTION(functionSerialize, 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 (JSC::JSValue binaryTypeValue = options->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "binaryType"_s))) { if (!binaryTypeValue.isString()) { - throwTypeError(globalObject, throwScope, - "binaryType must be a string"_s); + throwTypeError(globalObject, throwScope, "binaryType must be a string"_s); return {}; } @@ -782,8 +780,7 @@ JSC_DEFINE_HOST_FUNCTION(functionSerialize, Vector> transferList; Vector> dummyPorts; - ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), - dummyPorts); + ExceptionOr> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts); if (serialized.hasException()) { WebCore::propagateException(*globalObject, throwScope, diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 3893d6877b..25df8fd801 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -248,7 +248,7 @@ export function define( structuredClone = false, ...rest } = {} as Partial, -): Partial { +): ClassDefinition { return new ClassDefinition({ ...rest, call, diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 4e6cbd2bb9..a980fcfaff 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1795,7 +1795,7 @@ function generateZig( if (structuredClone) { exports.set("onStructuredCloneSerialize", symbolName(typeName, "onStructuredCloneSerialize")); - if (structuredClone === "transferable") { + if (typeof structuredClone === "object" && structuredClone.transferable) { exports.set("onStructuredCloneTransfer", symbolName(typeName, "onStructuredCloneTransfer")); } @@ -2029,7 +2029,7 @@ const JavaScriptCoreBindings = struct { } `; - if (structuredClone === "transferable") { + if (typeof structuredClone === "object" && structuredClone.transferable) { exports.set("structuredClone_transferable", symbolName(typeName, "onStructuredCloneTransfer")); output += ` pub fn ${exports.get("structuredClone_transferable")}(thisValue: *${typeName}, globalObject: *JSC.JSGlobalObject, ctx: *anyopaque, write: WriteBytesFn) callconv(JSC.conv) void { @@ -2288,6 +2288,25 @@ ${jsclasses }`; } +function isTransferableCppImpl() { + return ` +bool WebCore::SerializedScriptValue::isTransferable(JSC::JSGlobalObject* globalObject, JSC::JSValue value) +{ + if (!value.isCell()) return true; + auto cell = value.asCell(); +${classes + .map(c => { + if (c.structuredClone == null) return ""; + if (typeof c.structuredClone === "boolean") return ""; + if (c.structuredClone.transferable) return ""; + return ` if (JSC::jsDynamicCast(cell)) return false;\n`; + }) + .join("")} + return true; +} +`; +} + function initLazyClasses(initLaterFunctions) { return ` @@ -2352,7 +2371,7 @@ pub const WriteBytesFn = *const fn(*anyopaque, ptr: [*]const u8, len: u32) callc `; -const classes = []; +const classes: ClassDefinition[] = []; for (const file of files) { const result = require(path.resolve(file)); if (!(result?.default?.length ?? 0)) continue; @@ -2511,7 +2530,7 @@ fn log_zig_call(typename: []const u8, callframe: *JSC.CallFrame) callconv(bun.ca if (comptime Environment.enable_logs) { zig("{s}({d} args)", .{typename, callframe.arguments().len}); } -} +} fn log_zig_get_internal_properties(typename: []const u8) callconv(bun.callconv_inline) void { if (comptime Environment.enable_logs) { @@ -2602,6 +2621,7 @@ if (!process.env.ONLY_ZIG) { writeCppSerializers(classes), GENERATED_CLASSES_IMPL_FOOTER, jsInheritsCppImpl(), + isTransferableCppImpl(), ]); if (lutTextFile.length) { @@ -2704,7 +2724,7 @@ function getPropertySignatureWithComment( commentLines.push( ` Look for a setter like this: * \`\`\`zig - * fn ${propDef.accessor.setter}(this: *${classDef.name}, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!void + * fn ${propDef.accessor.setter}(this: *${classDef.name}, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!void * \`\`\``, ); if (propDef.cache) { @@ -2854,7 +2874,7 @@ export function generateBuiltinTypes(classes: ClassDefinition[]): string { * Type definitions for Bun's built-in classes implemented in Zig. * Do not edit this file directly. * @generated - * + * * This namespace does not exist at runtime! */ declare namespace $ZigGeneratedClasses { diff --git a/test/harness.ts b/test/harness.ts index 95d444209f..1ef9fedda8 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -210,7 +210,7 @@ export function tempDirWithFiles(basename: string, files: DirectoryTree): string return base; } -export function bunRun(file: string, env?: Record) { +export function bunRun(file: string, env?: Record | NodeJS.ProcessEnv) { var path = require("path"); const result = Bun.spawnSync([bunExe(), file], { cwd: path.dirname(file), diff --git a/test/js/node/cluster.test.ts b/test/js/node/cluster.test.ts new file mode 100644 index 0000000000..2e168261b7 --- /dev/null +++ b/test/js/node/cluster.test.ts @@ -0,0 +1,70 @@ +import { bunEnv, bunRun, joinP, tempDirWithFiles } from "harness"; + +test("cloneable and transferable equals", () => { + const dir = tempDirWithFiles("bun-test", { + "index.ts": ` +import cluster from "cluster"; +import { expect } from "bun:test"; +if (cluster.isPrimary) { + cluster.settings.serialization = "advanced"; + const worker = cluster.fork(); + const original = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]); + const buf = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]); + const ab = buf.buffer.transfer(); + expect(ab).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(ab)).toEqual(original); + worker.on("online", function () { + worker.send(ab); + }); + worker.on("message", function (data) { + worker.kill(); + expect(data).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(data)).toEqual(original); + process.exit(0); + }); +} else { + process.on("message", msg => { + console.log("W", msg); + process.send!(msg); + }); +} +`, + }); + bunRun(joinP(dir, "index.ts"), bunEnv); +}); + +test("cloneable and non-transferable not-equals", () => { + const dir = tempDirWithFiles("bun-test", { + "index.ts": ` +import cluster from "cluster"; +import { expect } from "bun:test"; +if (cluster.isPrimary) { + cluster.settings.serialization = "advanced"; + const worker = cluster.fork(); + const file = Bun.file(import.meta.filename); + console.log("P", "O", file); + expect(file).toBeInstanceOf(Blob); // Bun.BunFile isnt exposed to JS + expect(file.name).toEqual(import.meta.filename); + expect(file.type).toEqual("text/javascript;charset=utf-8"); + worker.on("online", function () { + worker.send({ file }); + }); + worker.on("message", function (data) { + worker.kill(); + const { file } = data; + console.log("P", "M", file); + expect(file.name).toBeUndefined(); + expect(file.type).toBeUndefined(); + expect(file).toBeEmptyObject(); + process.exit(0); + }); +} else { + process.on("message", msg => { + console.log("W", msg); + process.send!(msg); + }); +} +`, + }); + bunRun(joinP(dir, "index.ts"), bunEnv); +}); diff --git a/test/js/web/workers/message-channel.test.ts b/test/js/web/workers/message-channel.test.ts index 033343b34f..cebf3f9a4c 100644 --- a/test/js/web/workers/message-channel.test.ts +++ b/test/js/web/workers/message-channel.test.ts @@ -253,3 +253,50 @@ test("gc", () => { messageChannel.port2; } }); + +test("cloneable and transferable equals", async () => { + const assert = require("assert"); + const mc = new MessageChannel(); + const original = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]); + const buf = Uint8Array.from([21, 11, 96, 126, 243, 128, 164]); + const ab = buf.buffer.transfer(); + expect(ab).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(ab)).toEqual(original); + const { promise, resolve, reject } = Promise.withResolvers(); + mc.port1.onmessage = ({ data }) => { + try { + expect(data).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(data)).toEqual(original); + mc.port1.close(); + resolve(); + } catch (e) { + reject(e); + } + }; + mc.port2.postMessage(ab); + await promise; +}); + +test("cloneable and non-transferable equals", async () => { + const assert = require("assert"); + const mc = new MessageChannel(); + const file = Bun.file(import.meta.filename); + expect(file).toBeInstanceOf(Blob); // Bun.BunFile isnt exposed to JS + expect(file.name).toEqual(import.meta.filename); + expect(file.type).toEqual("text/javascript;charset=utf-8"); + const { promise, resolve, reject } = Promise.withResolvers(); + mc.port1.onmessage = ({ data }) => { + try { + expect(data).toBeInstanceOf(file.__proto__.constructor); + expect(data.name).toEqual(import.meta.filename); + expect(data.type).toEqual("text/javascript;charset=utf-8"); + // expect(data).not.toBeEmptyObject(); + mc.port1.close(); + resolve(); + } catch (e) { + reject(e); + } + }; + mc.port2.postMessage(file); + await promise; +});