Skip to content

Commit 4ef1d68

Browse files
joyeecheungUlisesGascon
authored andcommitted
src: implement structuredClone in native
Simplify the implementation by implementing it directly in C++. This improves performance and also makes structuredClone supported in custom snapshots. PR-URL: #50330 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Daeyeon Jeong <[email protected]>
1 parent d4be8fa commit 4ef1d68

File tree

8 files changed

+150
-82
lines changed

8 files changed

+150
-82
lines changed

benchmark/misc/structured-clone.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
type: ['string', 'object', 'arraybuffer'],
8+
n: [1e4],
9+
});
10+
11+
function main({ n, type }) {
12+
const data = [];
13+
14+
switch (type) {
15+
case 'string':
16+
for (let i = 0; i < n; ++i) {
17+
data.push(new Date().toISOString());
18+
}
19+
break;
20+
case 'object':
21+
for (let i = 0; i < n; ++i) {
22+
data.push({ ...process.config });
23+
}
24+
break;
25+
case 'arraybuffer':
26+
for (let i = 0; i < n; ++i) {
27+
data.push(new ArrayBuffer(10));
28+
}
29+
break;
30+
default:
31+
throw new Error('Unsupported payload type');
32+
}
33+
34+
const run = type === 'arraybuffer' ? (i) => {
35+
data[i] = structuredClone(data[i], { transfer: [ data[i] ] });
36+
} : (i) => {
37+
data[i] = structuredClone(data[i]);
38+
};
39+
40+
bench.start();
41+
for (let i = 0; i < n; ++i) {
42+
run(i);
43+
}
44+
bench.end(n);
45+
assert.strictEqual(data.length, n);
46+
}

lib/.eslintrc.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ rules:
172172
- name: setTimeout
173173
message: Use `const { setTimeout } = require('timers');` instead of the global.
174174
- name: structuredClone
175-
message: Use `const { structuredClone } = require('internal/structured_clone');` instead of the global.
175+
message: Use `const { structuredClone } = internalBinding('messaging');` instead of the global.
176176
- name: SubtleCrypto
177177
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
178178
no-restricted-modules:

lib/internal/bootstrap/web/exposed-window-or-worker.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,8 @@ const {
3131
} = require('internal/process/task_queues');
3232
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
3333

34-
defineLazyProperties(
35-
globalThis,
36-
'internal/structured_clone',
37-
['structuredClone'],
38-
);
34+
const { structuredClone } = internalBinding('messaging');
35+
defineOperation(globalThis, 'structuredClone', structuredClone);
3936
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);
4037

4138
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts

lib/internal/perf/usertiming.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const {
3131
},
3232
} = require('internal/errors');
3333

34-
const { structuredClone } = require('internal/structured_clone');
34+
const { structuredClone } = internalBinding('messaging');
3535
const {
3636
lazyDOMException,
3737
kEnumerableProperty,

lib/internal/structured_clone.js

-29
This file was deleted.

lib/internal/webstreams/readablestream.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ const {
8686
kControllerErrorFunction,
8787
} = require('internal/streams/utils');
8888

89-
const {
90-
structuredClone,
91-
} = require('internal/structured_clone');
89+
const { structuredClone } = internalBinding('messaging');
9290

9391
const {
9492
ArrayBufferViewGetBuffer,

src/node_messaging.cc

+87-25
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,47 @@ static Maybe<bool> ReadIterable(Environment* env,
999999
return Just(true);
10001000
}
10011001

1002+
bool GetTransferList(Environment* env,
1003+
Local<Context> context,
1004+
Local<Value> transfer_list_v,
1005+
TransferList* transfer_list_out) {
1006+
if (transfer_list_v->IsNullOrUndefined()) {
1007+
// Browsers ignore null or undefined, and otherwise accept an array or an
1008+
// options object.
1009+
return true;
1010+
}
1011+
1012+
if (!transfer_list_v->IsObject()) {
1013+
THROW_ERR_INVALID_ARG_TYPE(
1014+
env, "Optional transferList argument must be an iterable");
1015+
return false;
1016+
}
1017+
1018+
bool was_iterable;
1019+
if (!ReadIterable(env, context, *transfer_list_out, transfer_list_v)
1020+
.To(&was_iterable))
1021+
return false;
1022+
if (!was_iterable) {
1023+
Local<Value> transfer_option;
1024+
if (!transfer_list_v.As<Object>()
1025+
->Get(context, env->transfer_string())
1026+
.ToLocal(&transfer_option))
1027+
return false;
1028+
if (!transfer_option->IsUndefined()) {
1029+
if (!ReadIterable(env, context, *transfer_list_out, transfer_option)
1030+
.To(&was_iterable))
1031+
return false;
1032+
if (!was_iterable) {
1033+
THROW_ERR_INVALID_ARG_TYPE(
1034+
env, "Optional options.transfer argument must be an iterable");
1035+
return false;
1036+
}
1037+
}
1038+
}
1039+
1040+
return true;
1041+
}
1042+
10021043
void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
10031044
Environment* env = Environment::GetCurrent(args);
10041045
Local<Object> obj = args.This();
@@ -1009,33 +1050,10 @@ void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
10091050
"MessagePort.postMessage");
10101051
}
10111052

1012-
if (!args[1]->IsNullOrUndefined() && !args[1]->IsObject()) {
1013-
// Browsers ignore null or undefined, and otherwise accept an array or an
1014-
// options object.
1015-
return THROW_ERR_INVALID_ARG_TYPE(env,
1016-
"Optional transferList argument must be an iterable");
1017-
}
1018-
10191053
TransferList transfer_list;
1020-
if (args[1]->IsObject()) {
1021-
bool was_iterable;
1022-
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
1023-
return;
1024-
if (!was_iterable) {
1025-
Local<Value> transfer_option;
1026-
if (!args[1].As<Object>()->Get(context, env->transfer_string())
1027-
.ToLocal(&transfer_option)) return;
1028-
if (!transfer_option->IsUndefined()) {
1029-
if (!ReadIterable(env, context, transfer_list, transfer_option)
1030-
.To(&was_iterable)) return;
1031-
if (!was_iterable) {
1032-
return THROW_ERR_INVALID_ARG_TYPE(env,
1033-
"Optional options.transfer argument must be an iterable");
1034-
}
1035-
}
1036-
}
1054+
if (!GetTransferList(env, context, args[1], &transfer_list)) {
1055+
return;
10371056
}
1038-
10391057
MessagePort* port = Unwrap<MessagePort>(args.This());
10401058
// Even if the backing MessagePort object has already been deleted, we still
10411059
// want to serialize the message to ensure spec-compliant behavior w.r.t.
@@ -1472,6 +1490,48 @@ static void SetDeserializerCreateObjectFunction(
14721490
env->set_messaging_deserialize_create_object(args[0].As<Function>());
14731491
}
14741492

1493+
static void StructuredClone(const FunctionCallbackInfo<Value>& args) {
1494+
Isolate* isolate = args.GetIsolate();
1495+
Local<Context> context = isolate->GetCurrentContext();
1496+
Realm* realm = Realm::GetCurrent(context);
1497+
Environment* env = realm->env();
1498+
1499+
if (args.Length() == 0) {
1500+
return THROW_ERR_MISSING_ARGS(env, "The value argument must be specified");
1501+
}
1502+
1503+
Local<Value> value = args[0];
1504+
1505+
TransferList transfer_list;
1506+
if (!args[1]->IsNullOrUndefined()) {
1507+
if (!args[1]->IsObject()) {
1508+
return THROW_ERR_INVALID_ARG_TYPE(
1509+
env, "The options argument must be either an object or undefined");
1510+
}
1511+
Local<Object> options = args[1].As<Object>();
1512+
Local<Value> transfer_list_v;
1513+
if (!options->Get(context, env->transfer_string())
1514+
.ToLocal(&transfer_list_v)) {
1515+
return;
1516+
}
1517+
1518+
// TODO(joyeecheung): implement this in JS land to avoid the C++ -> JS
1519+
// cost to convert a sequence into an array.
1520+
if (!GetTransferList(env, context, transfer_list_v, &transfer_list)) {
1521+
return;
1522+
}
1523+
}
1524+
1525+
std::shared_ptr<Message> msg = std::make_shared<Message>();
1526+
Local<Value> result;
1527+
if (msg->Serialize(env, context, value, transfer_list, Local<Object>())
1528+
.IsNothing() ||
1529+
!msg->Deserialize(env, context, nullptr).ToLocal(&result)) {
1530+
return;
1531+
}
1532+
args.GetReturnValue().Set(result);
1533+
}
1534+
14751535
static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
14761536
Environment* env = Environment::GetCurrent(args);
14771537
if (!args.IsConstructCall()) {
@@ -1554,6 +1614,7 @@ static void InitMessaging(Local<Object> target,
15541614
"setDeserializerCreateObjectFunction",
15551615
SetDeserializerCreateObjectFunction);
15561616
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
1617+
SetMethod(context, target, "structuredClone", StructuredClone);
15571618

15581619
{
15591620
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
@@ -1578,6 +1639,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15781639
registry->Register(MessagePort::ReceiveMessage);
15791640
registry->Register(MessagePort::MoveToContext);
15801641
registry->Register(SetDeserializerCreateObjectFunction);
1642+
registry->Register(StructuredClone);
15811643
}
15821644

15831645
} // anonymous namespace
+12-18
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
// Flags: --expose-internals
21
'use strict';
3-
/* eslint-disable no-global-assign */
42

53
require('../common');
4+
const assert = require('assert');
65

7-
const {
8-
structuredClone: _structuredClone,
9-
} = require('internal/structured_clone');
6+
assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' });
7+
assert.throws(() => structuredClone(undefined, ''), { code: 'ERR_INVALID_ARG_TYPE' });
8+
assert.throws(() => structuredClone(undefined, 1), { code: 'ERR_INVALID_ARG_TYPE' });
9+
assert.throws(() => structuredClone(undefined, { transfer: 1 }), { code: 'ERR_INVALID_ARG_TYPE' });
10+
assert.throws(() => structuredClone(undefined, { transfer: '' }), { code: 'ERR_INVALID_ARG_TYPE' });
1011

11-
const {
12-
strictEqual,
13-
throws,
14-
} = require('assert');
15-
16-
strictEqual(globalThis.structuredClone, _structuredClone);
17-
structuredClone = undefined;
18-
strictEqual(globalThis.structuredClone, undefined);
19-
20-
// Restore the value for the known globals check.
21-
structuredClone = _structuredClone;
22-
23-
throws(() => _structuredClone(), /ERR_MISSING_ARGS/);
12+
// Options can be null or undefined.
13+
assert.strictEqual(structuredClone(undefined), undefined);
14+
assert.strictEqual(structuredClone(undefined, null), undefined);
15+
// Transfer can be null or undefined.
16+
assert.strictEqual(structuredClone(undefined, { transfer: null }), undefined);
17+
assert.strictEqual(structuredClone(undefined, { }), undefined);

0 commit comments

Comments
 (0)