mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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: https://github.com/nodejs/node/pull/50330 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
This commit is contained in:
parent
b6bced8e84
commit
c3a41d83de
8 changed files with 150 additions and 82 deletions
46
benchmark/misc/structured-clone.js
Normal file
46
benchmark/misc/structured-clone.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
const common = require('../common.js');
|
||||
const assert = require('assert');
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
type: ['string', 'object', 'arraybuffer'],
|
||||
n: [1e4],
|
||||
});
|
||||
|
||||
function main({ n, type }) {
|
||||
const data = [];
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
for (let i = 0; i < n; ++i) {
|
||||
data.push(new Date().toISOString());
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
for (let i = 0; i < n; ++i) {
|
||||
data.push({ ...process.config });
|
||||
}
|
||||
break;
|
||||
case 'arraybuffer':
|
||||
for (let i = 0; i < n; ++i) {
|
||||
data.push(new ArrayBuffer(10));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported payload type');
|
||||
}
|
||||
|
||||
const run = type === 'arraybuffer' ? (i) => {
|
||||
data[i] = structuredClone(data[i], { transfer: [ data[i] ] });
|
||||
} : (i) => {
|
||||
data[i] = structuredClone(data[i]);
|
||||
};
|
||||
|
||||
bench.start();
|
||||
for (let i = 0; i < n; ++i) {
|
||||
run(i);
|
||||
}
|
||||
bench.end(n);
|
||||
assert.strictEqual(data.length, n);
|
||||
}
|
|
@ -172,7 +172,7 @@ rules:
|
|||
- name: setTimeout
|
||||
message: Use `const { setTimeout } = require('timers');` instead of the global.
|
||||
- name: structuredClone
|
||||
message: Use `const { structuredClone } = require('internal/structured_clone');` instead of the global.
|
||||
message: Use `const { structuredClone } = internalBinding('messaging');` instead of the global.
|
||||
- name: SubtleCrypto
|
||||
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
|
||||
no-restricted-modules:
|
||||
|
|
|
@ -31,11 +31,8 @@ const {
|
|||
} = require('internal/process/task_queues');
|
||||
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
|
||||
|
||||
defineLazyProperties(
|
||||
globalThis,
|
||||
'internal/structured_clone',
|
||||
['structuredClone'],
|
||||
);
|
||||
const { structuredClone } = internalBinding('messaging');
|
||||
defineOperation(globalThis, 'structuredClone', structuredClone);
|
||||
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
|
||||
|
|
|
@ -31,7 +31,7 @@ const {
|
|||
},
|
||||
} = require('internal/errors');
|
||||
|
||||
const { structuredClone } = require('internal/structured_clone');
|
||||
const { structuredClone } = internalBinding('messaging');
|
||||
const {
|
||||
lazyDOMException,
|
||||
kEnumerableProperty,
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
codes: { ERR_MISSING_ARGS },
|
||||
} = require('internal/errors');
|
||||
|
||||
const {
|
||||
MessageChannel,
|
||||
receiveMessageOnPort,
|
||||
} = require('internal/worker/io');
|
||||
|
||||
let channel;
|
||||
function structuredClone(value, options = undefined) {
|
||||
if (arguments.length === 0) {
|
||||
throw new ERR_MISSING_ARGS('value');
|
||||
}
|
||||
|
||||
// TODO: Improve this with a more efficient solution that avoids
|
||||
// instantiating a MessageChannel
|
||||
channel ??= new MessageChannel();
|
||||
channel.port1.unref();
|
||||
channel.port2.unref();
|
||||
channel.port1.postMessage(value, options?.transfer);
|
||||
return receiveMessageOnPort(channel.port2).message;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
structuredClone,
|
||||
};
|
|
@ -85,9 +85,7 @@ const {
|
|||
kControllerErrorFunction,
|
||||
} = require('internal/streams/utils');
|
||||
|
||||
const {
|
||||
structuredClone,
|
||||
} = require('internal/structured_clone');
|
||||
const { structuredClone } = internalBinding('messaging');
|
||||
|
||||
const {
|
||||
ArrayBufferViewGetBuffer,
|
||||
|
|
|
@ -1008,6 +1008,47 @@ static Maybe<bool> ReadIterable(Environment* env,
|
|||
return Just(true);
|
||||
}
|
||||
|
||||
bool GetTransferList(Environment* env,
|
||||
Local<Context> context,
|
||||
Local<Value> transfer_list_v,
|
||||
TransferList* transfer_list_out) {
|
||||
if (transfer_list_v->IsNullOrUndefined()) {
|
||||
// Browsers ignore null or undefined, and otherwise accept an array or an
|
||||
// options object.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!transfer_list_v->IsObject()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
env, "Optional transferList argument must be an iterable");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool was_iterable;
|
||||
if (!ReadIterable(env, context, *transfer_list_out, transfer_list_v)
|
||||
.To(&was_iterable))
|
||||
return false;
|
||||
if (!was_iterable) {
|
||||
Local<Value> transfer_option;
|
||||
if (!transfer_list_v.As<Object>()
|
||||
->Get(context, env->transfer_string())
|
||||
.ToLocal(&transfer_option))
|
||||
return false;
|
||||
if (!transfer_option->IsUndefined()) {
|
||||
if (!ReadIterable(env, context, *transfer_list_out, transfer_option)
|
||||
.To(&was_iterable))
|
||||
return false;
|
||||
if (!was_iterable) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(
|
||||
env, "Optional options.transfer argument must be an iterable");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Local<Object> obj = args.This();
|
||||
|
@ -1018,33 +1059,10 @@ void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
|
|||
"MessagePort.postMessage");
|
||||
}
|
||||
|
||||
if (!args[1]->IsNullOrUndefined() && !args[1]->IsObject()) {
|
||||
// Browsers ignore null or undefined, and otherwise accept an array or an
|
||||
// options object.
|
||||
return THROW_ERR_INVALID_ARG_TYPE(env,
|
||||
"Optional transferList argument must be an iterable");
|
||||
}
|
||||
|
||||
TransferList transfer_list;
|
||||
if (args[1]->IsObject()) {
|
||||
bool was_iterable;
|
||||
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
|
||||
return;
|
||||
if (!was_iterable) {
|
||||
Local<Value> transfer_option;
|
||||
if (!args[1].As<Object>()->Get(context, env->transfer_string())
|
||||
.ToLocal(&transfer_option)) return;
|
||||
if (!transfer_option->IsUndefined()) {
|
||||
if (!ReadIterable(env, context, transfer_list, transfer_option)
|
||||
.To(&was_iterable)) return;
|
||||
if (!was_iterable) {
|
||||
return THROW_ERR_INVALID_ARG_TYPE(env,
|
||||
"Optional options.transfer argument must be an iterable");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!GetTransferList(env, context, args[1], &transfer_list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MessagePort* port = Unwrap<MessagePort>(args.This());
|
||||
// Even if the backing MessagePort object has already been deleted, we still
|
||||
// want to serialize the message to ensure spec-compliant behavior w.r.t.
|
||||
|
@ -1535,6 +1553,48 @@ static void SetDeserializerCreateObjectFunction(
|
|||
env->set_messaging_deserialize_create_object(args[0].As<Function>());
|
||||
}
|
||||
|
||||
static void StructuredClone(const FunctionCallbackInfo<Value>& args) {
|
||||
Isolate* isolate = args.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
Realm* realm = Realm::GetCurrent(context);
|
||||
Environment* env = realm->env();
|
||||
|
||||
if (args.Length() == 0) {
|
||||
return THROW_ERR_MISSING_ARGS(env, "The value argument must be specified");
|
||||
}
|
||||
|
||||
Local<Value> value = args[0];
|
||||
|
||||
TransferList transfer_list;
|
||||
if (!args[1]->IsNullOrUndefined()) {
|
||||
if (!args[1]->IsObject()) {
|
||||
return THROW_ERR_INVALID_ARG_TYPE(
|
||||
env, "The options argument must be either an object or undefined");
|
||||
}
|
||||
Local<Object> options = args[1].As<Object>();
|
||||
Local<Value> transfer_list_v;
|
||||
if (!options->Get(context, env->transfer_string())
|
||||
.ToLocal(&transfer_list_v)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(joyeecheung): implement this in JS land to avoid the C++ -> JS
|
||||
// cost to convert a sequence into an array.
|
||||
if (!GetTransferList(env, context, transfer_list_v, &transfer_list)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Message> msg = std::make_shared<Message>();
|
||||
Local<Value> result;
|
||||
if (msg->Serialize(env, context, value, transfer_list, Local<Object>())
|
||||
.IsNothing() ||
|
||||
!msg->Deserialize(env, context, nullptr).ToLocal(&result)) {
|
||||
return;
|
||||
}
|
||||
args.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
if (!args.IsConstructCall()) {
|
||||
|
@ -1615,6 +1675,7 @@ static void InitMessaging(Local<Object> target,
|
|||
"setDeserializerCreateObjectFunction",
|
||||
SetDeserializerCreateObjectFunction);
|
||||
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
|
||||
SetMethod(context, target, "structuredClone", StructuredClone);
|
||||
|
||||
{
|
||||
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
|
||||
|
@ -1638,6 +1699,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
|||
registry->Register(MessagePort::ReceiveMessage);
|
||||
registry->Register(MessagePort::MoveToContext);
|
||||
registry->Register(SetDeserializerCreateObjectFunction);
|
||||
registry->Register(StructuredClone);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
// Flags: --expose-internals
|
||||
'use strict';
|
||||
/* eslint-disable no-global-assign */
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
const {
|
||||
structuredClone: _structuredClone,
|
||||
} = require('internal/structured_clone');
|
||||
assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' });
|
||||
assert.throws(() => structuredClone(undefined, ''), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
assert.throws(() => structuredClone(undefined, 1), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
assert.throws(() => structuredClone(undefined, { transfer: 1 }), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
assert.throws(() => structuredClone(undefined, { transfer: '' }), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
|
||||
const {
|
||||
strictEqual,
|
||||
throws,
|
||||
} = require('assert');
|
||||
|
||||
strictEqual(globalThis.structuredClone, _structuredClone);
|
||||
structuredClone = undefined;
|
||||
strictEqual(globalThis.structuredClone, undefined);
|
||||
|
||||
// Restore the value for the known globals check.
|
||||
structuredClone = _structuredClone;
|
||||
|
||||
throws(() => _structuredClone(), /ERR_MISSING_ARGS/);
|
||||
// Options can be null or undefined.
|
||||
assert.strictEqual(structuredClone(undefined), undefined);
|
||||
assert.strictEqual(structuredClone(undefined, null), undefined);
|
||||
// Transfer can be null or undefined.
|
||||
assert.strictEqual(structuredClone(undefined, { transfer: null }), undefined);
|
||||
assert.strictEqual(structuredClone(undefined, { }), undefined);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue