js: fix serialization of non-transferable objects (#19351)

This commit is contained in:
Meghan Denny
2025-04-29 17:23:26 -08:00
committed by GitHub
parent 3ea7953474
commit 2a2247bbb6
12 changed files with 186 additions and 42 deletions

View File

@@ -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

View File

@@ -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<JSC::Strong<JSC::JSObject>> transferList;
Vector<RefPtr<MessagePort>> dummyPorts;
ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList),
dummyPorts);
auto forStorage = SerializationForStorage::No;
auto context = SerializationContext::Default;
auto forTransferEnum = forTransferBool ? SerializationForTransfer::Yes : SerializationForTransfer::No;
ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts, forStorage, context, forTransferEnum);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

View File

@@ -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);

View File

@@ -887,7 +887,7 @@ public:
WasmMemoryHandleArray& wasmMemoryHandles,
#endif
Vector<uint8_t>& 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<uint8_t>& out, SerializationContext context, ArrayBufferContentsArray& sharedBuffers, SerializationForStorage forStorage)
Vector<uint8_t>& 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<uint8_t> 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<RefPtr<WebCodecsVideoFrame>>& 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<Ref<RTCDataChannel>>& channels
}
#endif
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext)
RefPtr<SerializedScriptValue> SerializedScriptValue::create(JSC::JSGlobalObject& globalObject, JSC::JSValue value, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext serializationContext, SerializationForTransfer forTransfer)
{
Vector<RefPtr<MessagePort>> 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> SerializedScriptValue::create(JSC::JSGlobalObject&
// return create(globalObject, value, WTFMove(transferList), messagePorts, forStorage, SerializationErrorMode::NonThrowing, serializationContext);
// }
ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector<JSC::Strong<JSC::JSObject>>&& transferList, Vector<RefPtr<MessagePort>>& messagePorts, SerializationForStorage forStorage, SerializationContext serializationContext)
ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalObject& globalObject, JSValue value, Vector<JSC::Strong<JSC::JSObject>>&& transferList, Vector<RefPtr<MessagePort>>& 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<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector<JSC::Strong<JSC::JSObject>>&& transferList, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context)
ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector<JSC::Strong<JSC::JSObject>>&& transferList, Vector<RefPtr<MessagePort>>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context)
ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalObject& lexicalGlobalObject, JSValue value, Vector<JSC::Strong<JSC::JSObject>>&& transferList, Vector<RefPtr<MessagePort>>& messagePorts, SerializationForStorage forStorage, SerializationErrorMode throwExceptions, SerializationContext context, SerializationForTransfer forTransfer)
{
VM& vm = lexicalGlobalObject.vm();
Vector<RefPtr<JSC::ArrayBuffer>> arrayBuffers;
@@ -5828,7 +5835,7 @@ ExceptionOr<Ref<SerializedScriptValue>> 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<Ref<SerializedScriptValue>> 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<String>& blobURLs, const Vector<String>& blobFilePaths, SerializationErrorMode throwExceptions, bool* didFail)
// {

View File

@@ -75,6 +75,8 @@ enum class SerializationContext { Default,
WindowPostMessage };
enum class SerializationForStorage : bool { No,
Yes };
enum class SerializationForTransfer : bool { No,
Yes };
using ArrayBufferContentsArray = Vector<JSC::ArrayBufferContents>;
#if ENABLE(WEBASSEMBLY)
@@ -88,11 +90,12 @@ class SerializedScriptValue : public ThreadSafeRefCounted<SerializedScriptValue>
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<Ref<SerializedScriptValue>> create(JSC::JSGlobalObject&, JSC::JSValue, Vector<JSC::Strong<JSC::JSObject>>&& transfer, Vector<RefPtr<MessagePort>>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default);
WEBCORE_EXPORT static ExceptionOr<Ref<SerializedScriptValue>> create(JSC::JSGlobalObject&, JSC::JSValue, Vector<JSC::Strong<JSC::JSObject>>&& transfer, Vector<RefPtr<MessagePort>>&, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No);
// WEBCORE_EXPORT static ExceptionOr<Ref<SerializedScriptValue>> create(JSC::JSGlobalObject&, JSC::JSValue, Vector<JSC::Strong<JSC::JSObject>>&& transfer, SerializationForStorage = SerializationForStorage::No, SerializationContext = SerializationContext::Default);
WEBCORE_EXPORT static RefPtr<SerializedScriptValue> create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default);
WEBCORE_EXPORT static RefPtr<SerializedScriptValue> create(JSC::JSGlobalObject&, JSC::JSValue, SerializationForStorage = SerializationForStorage::No, SerializationErrorMode = SerializationErrorMode::Throwing, SerializationContext = SerializationContext::Default, SerializationForTransfer = SerializationForTransfer::No);
static RefPtr<SerializedScriptValue> convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { return create(globalObject, value, SerializationForStorage::Yes); }
@@ -148,7 +151,7 @@ private:
// Vector<RefPtr<WebCodecsEncodedVideoChunkStorage>>&& = {}, Vector<WebCodecsVideoFrameData>&& = {}
// #endif
// );
static ExceptionOr<Ref<SerializedScriptValue>> create(JSC::JSGlobalObject&, JSC::JSValue, Vector<JSC::Strong<JSC::JSObject>>&& transfer, Vector<RefPtr<MessagePort>>&, SerializationForStorage, SerializationErrorMode, SerializationContext);
static ExceptionOr<Ref<SerializedScriptValue>> create(JSC::JSGlobalObject&, JSC::JSValue, Vector<JSC::Strong<JSC::JSObject>>&& transfer, Vector<RefPtr<MessagePort>>&, SerializationForStorage, SerializationErrorMode, SerializationContext, SerializationForTransfer);
WEBCORE_EXPORT SerializedScriptValue(Vector<unsigned char>&&, std::unique_ptr<ArrayBufferContentsArray>&& = nullptr
#if ENABLE(WEB_RTC)
,

View File

@@ -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);

View File

@@ -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<JSC::Strong<JSC::JSObject>> transferList;
Vector<RefPtr<MessagePort>> dummyPorts;
ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList),
dummyPorts);
ExceptionOr<Ref<SerializedScriptValue>> serialized = SerializedScriptValue::create(*globalObject, value, WTFMove(transferList), dummyPorts);
if (serialized.hasException()) {
WebCore::propagateException(*globalObject, throwScope,

View File

@@ -248,7 +248,7 @@ export function define(
structuredClone = false,
...rest
} = {} as Partial<ClassDefinition>,
): Partial<ClassDefinition> {
): ClassDefinition {
return new ClassDefinition({
...rest,
call,

View File

@@ -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<WebCore::JS${c.name}*>(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("<d>{s}<d>({d} args)<r>", .{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 {

View File

@@ -210,7 +210,7 @@ export function tempDirWithFiles(basename: string, files: DirectoryTree): string
return base;
}
export function bunRun(file: string, env?: Record<string, string>) {
export function bunRun(file: string, env?: Record<string, string> | NodeJS.ProcessEnv) {
var path = require("path");
const result = Bun.spawnSync([bunExe(), file], {
cwd: path.dirname(file),

View File

@@ -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);
});

View File

@@ -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;
});