Skip to content

Commit 4650330

Browse files
bartlomiejuclaude
andauthored
feat(core): add cloneable resource registry for structured clone (#32672)
## Summary - Adds infrastructure for custom JS objects to support structured cloning via `postMessage`/`MessageChannel` - Objects set `[core.hostObjectBrand]` to a serializer function returning `{ type: "<name>", ...data }`, then register a deserializer via `core.registerCloneableResource(name, fn)` - Marks `op_deserialize` as reentrant so deserializer callbacks can invoke ops - Passes registered cloneable deserializers during message port deserialization This enables objects like `CryptoKey` (#12734) and `X509Certificate` to be cloned across message ports, addressing #12067. Ref: #12067 Ref: #12734 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d05945 commit 4650330

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

ext/web/13_message_port.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ function deserializeJsMessageData(messageData) {
409409
};
410410
}
411411

412+
const deserializers = core.getCloneableDeserializers();
413+
if (!options) {
414+
options = { deserializers };
415+
} else {
416+
options.deserializers = deserializers;
417+
}
412418
const data = core.deserialize(messageData.data, options);
413419

414420
for (let i = 0; i < arrayBufferIdsInTransferables.length; ++i) {

libs/core/01_core.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,14 @@
683683
transferableResources[name] = { send, receive };
684684
};
685685
const getTransferableResource = (name) => transferableResources[name];
686+
const cloneableDeserializers = { __proto__: null };
687+
const registerCloneableResource = (name, deserialize) => {
688+
if (cloneableDeserializers[name]) {
689+
throw new Error(`${name} is already registered`);
690+
}
691+
cloneableDeserializers[name] = deserialize;
692+
};
693+
const getCloneableDeserializers = () => cloneableDeserializers;
686694

687695
// A helper function that will bind our own console implementation
688696
// with default implementation of Console from V8. This will cause
@@ -1025,11 +1033,13 @@
10251033
hostObjectBrand,
10261034
registerTransferableResource,
10271035
getTransferableResource,
1036+
registerCloneableResource,
1037+
getCloneableDeserializers,
10281038
encode: (text) => op_encode(text),
10291039
encodeBinaryString: (buffer) => op_encode_binary_string(buffer),
10301040
decode: (buffer) => op_decode(buffer),
10311041
structuredClone: (value, deserializers) =>
1032-
op_structured_clone(value, deserializers),
1042+
op_structured_clone(value, deserializers ?? cloneableDeserializers),
10331043
serialize: (
10341044
value,
10351045
options,

libs/core/ops_builtin_v8.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ pub fn op_serialize<'s, 'i>(
848848
}
849849
}
850850

851-
#[op2]
851+
#[op2(reentrant)]
852852
pub fn op_deserialize<'s, 'i>(
853853
scope: &mut v8::PinScope<'s, 'i>,
854854
#[buffer] zero_copy: JsBuffer,

libs/core_testing/unit/serialize_deserialize_test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
// Copyright 2018-2026 the Deno authors. MIT license.
2-
import { assertArrayEquals, assertEquals, test } from "checkin:testing";
2+
import {
3+
assert,
4+
assertArrayEquals,
5+
assertEquals,
6+
assertThrows,
7+
test,
8+
} from "checkin:testing";
39

410
test(function testIssue20727() {
511
// https://github.com/denoland/deno/issues/20727
@@ -109,3 +115,103 @@ test(function structuredClone() {
109115
assertEquals(cloned.test2, circularObject.test2);
110116
assertEquals(cloned.test3, circularObject.test3);
111117
});
118+
119+
test(function cloneableResourceStructuredClone() {
120+
// Create a class that supports structured cloning via hostObjectBrand
121+
class MyCloneable {
122+
value: string;
123+
constructor(value: string) {
124+
this.value = value;
125+
// deno-lint-ignore no-this-alias
126+
const self = this;
127+
this[Deno.core.hostObjectBrand] = () => ({
128+
type: "MyCloneable",
129+
value: self.value,
130+
});
131+
}
132+
}
133+
134+
Deno.core.registerCloneableResource(
135+
"MyCloneable",
136+
(data: { value: string }) => new MyCloneable(data.value),
137+
);
138+
139+
const original = new MyCloneable("hello");
140+
const cloned = Deno.core.structuredClone(original);
141+
142+
assert(cloned instanceof MyCloneable);
143+
assertEquals(cloned.value, "hello");
144+
assert(cloned !== original);
145+
});
146+
147+
test(function cloneableResourceSerializeDeserialize() {
148+
// Use a different name to avoid duplicate registration
149+
class AnotherCloneable {
150+
data: number;
151+
constructor(data: number) {
152+
this.data = data;
153+
// deno-lint-ignore no-this-alias
154+
const self = this;
155+
this[Deno.core.hostObjectBrand] = () => ({
156+
type: "AnotherCloneable",
157+
data: self.data,
158+
});
159+
}
160+
}
161+
162+
Deno.core.registerCloneableResource(
163+
"AnotherCloneable",
164+
(d: { data: number }) => new AnotherCloneable(d.data),
165+
);
166+
167+
const original = new AnotherCloneable(42);
168+
const serialized = Deno.core.serialize(original);
169+
const deserialized = Deno.core.deserialize(serialized, {
170+
deserializers: Deno.core.getCloneableDeserializers(),
171+
});
172+
173+
assert(deserialized instanceof AnotherCloneable);
174+
assertEquals(deserialized.data, 42);
175+
});
176+
177+
test(function cloneableResourceNestedInObject() {
178+
class NestedCloneable {
179+
name: string;
180+
constructor(name: string) {
181+
this.name = name;
182+
// deno-lint-ignore no-this-alias
183+
const self = this;
184+
this[Deno.core.hostObjectBrand] = () => ({
185+
type: "NestedCloneable",
186+
name: self.name,
187+
});
188+
}
189+
}
190+
191+
Deno.core.registerCloneableResource(
192+
"NestedCloneable",
193+
(d: { name: string }) => new NestedCloneable(d.name),
194+
);
195+
196+
const obj = {
197+
foo: "bar",
198+
nested: new NestedCloneable("test"),
199+
num: 123,
200+
};
201+
202+
const cloned = Deno.core.structuredClone(obj);
203+
204+
assertEquals(cloned.foo, "bar");
205+
assertEquals(cloned.num, 123);
206+
assert(cloned.nested instanceof NestedCloneable);
207+
assertEquals(cloned.nested.name, "test");
208+
});
209+
210+
test(function cloneableResourceDuplicateRegistrationThrows() {
211+
Deno.core.registerCloneableResource("DuplicateTest", () => {});
212+
assertThrows(
213+
() => Deno.core.registerCloneableResource("DuplicateTest", () => {}),
214+
Error,
215+
"already registered",
216+
);
217+
});

0 commit comments

Comments
 (0)