Skip to content

Commit d7538ee

Browse files
committed
src: implement structuredClone in native
Simplify the implementation by implementing it directly in C++.
1 parent 0cec822 commit d7538ee

File tree

7 files changed

+150
-81
lines changed

7 files changed

+150
-81
lines changed

benchmark/misc/structured-clone.js

Lines changed: 46 additions & 0 deletions
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/internal/bootstrap/web/exposed-window-or-worker.js

Lines changed: 2 additions & 5 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 0 additions & 29 deletions
This file was deleted.

lib/internal/webstreams/readablestream.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@ const {
8585
kControllerErrorFunction,
8686
} = require('internal/streams/utils');
8787

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

9290
const {
9391
ArrayBufferViewGetBuffer,

src/node_messaging.cc

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,47 @@ static Maybe<bool> ReadIterable(Environment* env,
10011001
return Just(true);
10021002
}
10031003

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

1014-
if (!args[1]->IsNullOrUndefined() && !args[1]->IsObject()) {
1015-
// Browsers ignore null or undefined, and otherwise accept an array or an
1016-
// options object.
1017-
return THROW_ERR_INVALID_ARG_TYPE(env,
1018-
"Optional transferList argument must be an iterable");
1019-
}
1020-
10211055
TransferList transfer_list;
1022-
if (args[1]->IsObject()) {
1023-
bool was_iterable;
1024-
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
1025-
return;
1026-
if (!was_iterable) {
1027-
Local<Value> transfer_option;
1028-
if (!args[1].As<Object>()->Get(context, env->transfer_string())
1029-
.ToLocal(&transfer_option)) return;
1030-
if (!transfer_option->IsUndefined()) {
1031-
if (!ReadIterable(env, context, transfer_list, transfer_option)
1032-
.To(&was_iterable)) return;
1033-
if (!was_iterable) {
1034-
return THROW_ERR_INVALID_ARG_TYPE(env,
1035-
"Optional options.transfer argument must be an iterable");
1036-
}
1037-
}
1038-
}
1056+
if (!GetTransferList(env, context, args[1], &transfer_list)) {
1057+
return;
10391058
}
1040-
10411059
MessagePort* port = Unwrap<MessagePort>(args.This());
10421060
// Even if the backing MessagePort object has already been deleted, we still
10431061
// want to serialize the message to ensure spec-compliant behavior w.r.t.
@@ -1528,6 +1546,49 @@ static void SetDeserializerCreateObjectFunction(
15281546
env->set_messaging_deserialize_create_object(args[0].As<Function>());
15291547
}
15301548

1549+
static void StructuredClone(const FunctionCallbackInfo<Value>& args) {
1550+
Isolate* isolate = args.GetIsolate();
1551+
Local<Context> context = isolate->GetCurrentContext();
1552+
Realm* realm = Realm::GetCurrent(context);
1553+
Environment* env = realm->env();
1554+
1555+
if (args.Length() == 0) {
1556+
return THROW_ERR_MISSING_ARGS(env, "The value argument must be specified");
1557+
}
1558+
1559+
Local<Value> value = args[0];
1560+
1561+
TransferList transfer_list;
1562+
if (!args[1]->IsNullOrUndefined()) {
1563+
if (!args[1]->IsObject()) {
1564+
return THROW_ERR_INVALID_ARG_TYPE(
1565+
env, "The options argument must be either an object or undefined");
1566+
}
1567+
Local<Object> options = args[1].As<Object>();
1568+
Local<Value> transfer_list_v;
1569+
if (!options->Get(context, FIXED_ONE_BYTE_STRING(isolate, "transfer"))
1570+
.ToLocal(&transfer_list_v)) {
1571+
return;
1572+
}
1573+
1574+
if (!GetTransferList(env, context, transfer_list_v, &transfer_list)) {
1575+
return;
1576+
}
1577+
}
1578+
1579+
std::shared_ptr<Message> msg = std::make_shared<Message>();
1580+
Maybe<bool> serialization_maybe =
1581+
msg->Serialize(env, context, value, transfer_list, Local<Object>());
1582+
1583+
if (serialization_maybe.IsNothing()) {
1584+
return;
1585+
}
1586+
Local<Value> result;
1587+
if (msg->Deserialize(env, context, nullptr).ToLocal(&result)) {
1588+
args.GetReturnValue().Set(result);
1589+
}
1590+
}
1591+
15311592
static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
15321593
Environment* env = Environment::GetCurrent(args);
15331594
if (!args.IsConstructCall()) {
@@ -1608,6 +1669,7 @@ static void InitMessaging(Local<Object> target,
16081669
"setDeserializerCreateObjectFunction",
16091670
SetDeserializerCreateObjectFunction);
16101671
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
1672+
SetMethod(context, target, "structuredClone", StructuredClone);
16111673

16121674
{
16131675
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
@@ -1631,6 +1693,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16311693
registry->Register(MessagePort::ReceiveMessage);
16321694
registry->Register(MessagePort::MoveToContext);
16331695
registry->Register(SetDeserializerCreateObjectFunction);
1696+
registry->Register(StructuredClone);
16341697
}
16351698

16361699
} // anonymous namespace
Lines changed: 12 additions & 18 deletions
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)