Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ext/web/13_message_port.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,12 @@ function deserializeJsMessageData(messageData) {
};
}

const deserializers = core.getCloneableDeserializers();
if (!options) {
options = { deserializers };
} else {
options.deserializers = deserializers;
}
const data = core.deserialize(messageData.data, options);

for (let i = 0; i < arrayBufferIdsInTransferables.length; ++i) {
Expand Down
12 changes: 11 additions & 1 deletion libs/core/01_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,14 @@
transferableResources[name] = { send, receive };
};
const getTransferableResource = (name) => transferableResources[name];
const cloneableDeserializers = { __proto__: null };
const registerCloneableResource = (name, deserialize) => {
if (cloneableDeserializers[name]) {
throw new Error(`${name} is already registered`);
}
cloneableDeserializers[name] = deserialize;
};
const getCloneableDeserializers = () => cloneableDeserializers;

// A helper function that will bind our own console implementation
// with default implementation of Console from V8. This will cause
Expand Down Expand Up @@ -1025,11 +1033,13 @@
hostObjectBrand,
registerTransferableResource,
getTransferableResource,
registerCloneableResource,
getCloneableDeserializers,
encode: (text) => op_encode(text),
encodeBinaryString: (buffer) => op_encode_binary_string(buffer),
decode: (buffer) => op_decode(buffer),
structuredClone: (value, deserializers) =>
op_structured_clone(value, deserializers),
op_structured_clone(value, deserializers ?? cloneableDeserializers),
serialize: (
value,
options,
Expand Down
2 changes: 1 addition & 1 deletion libs/core/ops_builtin_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ pub fn op_serialize<'s, 'i>(
}
}

#[op2]
#[op2(reentrant)]
pub fn op_deserialize<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
#[buffer] zero_copy: JsBuffer,
Expand Down
108 changes: 107 additions & 1 deletion libs/core_testing/unit/serialize_deserialize_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Copyright 2018-2026 the Deno authors. MIT license.
import { assertArrayEquals, assertEquals, test } from "checkin:testing";
import {
assert,
assertArrayEquals,
assertEquals,
assertThrows,
test,
} from "checkin:testing";

test(function testIssue20727() {
// https://github.com/denoland/deno/issues/20727
Expand Down Expand Up @@ -109,3 +115,103 @@ test(function structuredClone() {
assertEquals(cloned.test2, circularObject.test2);
assertEquals(cloned.test3, circularObject.test3);
});

test(function cloneableResourceStructuredClone() {
// Create a class that supports structured cloning via hostObjectBrand
class MyCloneable {
value: string;
constructor(value: string) {
this.value = value;
// deno-lint-ignore no-this-alias
const self = this;
this[Deno.core.hostObjectBrand] = () => ({
type: "MyCloneable",
value: self.value,
});
}
}

Deno.core.registerCloneableResource(
"MyCloneable",
(data: { value: string }) => new MyCloneable(data.value),
);

const original = new MyCloneable("hello");
const cloned = Deno.core.structuredClone(original);

assert(cloned instanceof MyCloneable);
assertEquals(cloned.value, "hello");
assert(cloned !== original);
});

test(function cloneableResourceSerializeDeserialize() {
// Use a different name to avoid duplicate registration
class AnotherCloneable {
data: number;
constructor(data: number) {
this.data = data;
// deno-lint-ignore no-this-alias
const self = this;
this[Deno.core.hostObjectBrand] = () => ({
type: "AnotherCloneable",
data: self.data,
});
}
}

Deno.core.registerCloneableResource(
"AnotherCloneable",
(d: { data: number }) => new AnotherCloneable(d.data),
);

const original = new AnotherCloneable(42);
const serialized = Deno.core.serialize(original);
const deserialized = Deno.core.deserialize(serialized, {
deserializers: Deno.core.getCloneableDeserializers(),
});

assert(deserialized instanceof AnotherCloneable);
assertEquals(deserialized.data, 42);
});

test(function cloneableResourceNestedInObject() {
class NestedCloneable {
name: string;
constructor(name: string) {
this.name = name;
// deno-lint-ignore no-this-alias
const self = this;
this[Deno.core.hostObjectBrand] = () => ({
type: "NestedCloneable",
name: self.name,
});
}
}

Deno.core.registerCloneableResource(
"NestedCloneable",
(d: { name: string }) => new NestedCloneable(d.name),
);

const obj = {
foo: "bar",
nested: new NestedCloneable("test"),
num: 123,
};

const cloned = Deno.core.structuredClone(obj);

assertEquals(cloned.foo, "bar");
assertEquals(cloned.num, 123);
assert(cloned.nested instanceof NestedCloneable);
assertEquals(cloned.nested.name, "test");
});

test(function cloneableResourceDuplicateRegistrationThrows() {
Deno.core.registerCloneableResource("DuplicateTest", () => {});
assertThrows(
() => Deno.core.registerCloneableResource("DuplicateTest", () => {}),
Error,
"already registered",
);
});
Loading