mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01: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
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
|
- name: setTimeout
|
||||||
message: Use `const { setTimeout } = require('timers');` instead of the global.
|
message: Use `const { setTimeout } = require('timers');` instead of the global.
|
||||||
- name: structuredClone
|
- 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
|
- name: SubtleCrypto
|
||||||
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
|
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
|
||||||
no-restricted-modules:
|
no-restricted-modules:
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,8 @@ const {
|
||||||
} = require('internal/process/task_queues');
|
} = require('internal/process/task_queues');
|
||||||
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
|
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
|
||||||
|
|
||||||
defineLazyProperties(
|
const { structuredClone } = internalBinding('messaging');
|
||||||
globalThis,
|
defineOperation(globalThis, 'structuredClone', structuredClone);
|
||||||
'internal/structured_clone',
|
|
||||||
['structuredClone'],
|
|
||||||
);
|
|
||||||
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);
|
defineLazyProperties(globalThis, 'buffer', ['atob', 'btoa']);
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
|
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const {
|
||||||
},
|
},
|
||||||
} = require('internal/errors');
|
} = require('internal/errors');
|
||||||
|
|
||||||
const { structuredClone } = require('internal/structured_clone');
|
const { structuredClone } = internalBinding('messaging');
|
||||||
const {
|
const {
|
||||||
lazyDOMException,
|
lazyDOMException,
|
||||||
kEnumerableProperty,
|
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,
|
kControllerErrorFunction,
|
||||||
} = require('internal/streams/utils');
|
} = require('internal/streams/utils');
|
||||||
|
|
||||||
const {
|
const { structuredClone } = internalBinding('messaging');
|
||||||
structuredClone,
|
|
||||||
} = require('internal/structured_clone');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ArrayBufferViewGetBuffer,
|
ArrayBufferViewGetBuffer,
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,47 @@ static Maybe<bool> ReadIterable(Environment* env,
|
||||||
return Just(true);
|
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) {
|
void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
Local<Object> obj = args.This();
|
Local<Object> obj = args.This();
|
||||||
|
|
@ -1018,33 +1059,10 @@ void MessagePort::PostMessage(const FunctionCallbackInfo<Value>& args) {
|
||||||
"MessagePort.postMessage");
|
"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;
|
TransferList transfer_list;
|
||||||
if (args[1]->IsObject()) {
|
if (!GetTransferList(env, context, args[1], &transfer_list)) {
|
||||||
bool was_iterable;
|
|
||||||
if (!ReadIterable(env, context, transfer_list, args[1]).To(&was_iterable))
|
|
||||||
return;
|
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");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessagePort* port = Unwrap<MessagePort>(args.This());
|
MessagePort* port = Unwrap<MessagePort>(args.This());
|
||||||
// Even if the backing MessagePort object has already been deleted, we still
|
// 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.
|
// 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>());
|
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) {
|
static void MessageChannel(const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
if (!args.IsConstructCall()) {
|
if (!args.IsConstructCall()) {
|
||||||
|
|
@ -1615,6 +1675,7 @@ static void InitMessaging(Local<Object> target,
|
||||||
"setDeserializerCreateObjectFunction",
|
"setDeserializerCreateObjectFunction",
|
||||||
SetDeserializerCreateObjectFunction);
|
SetDeserializerCreateObjectFunction);
|
||||||
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
|
SetMethod(context, target, "broadcastChannel", BroadcastChannel);
|
||||||
|
SetMethod(context, target, "structuredClone", StructuredClone);
|
||||||
|
|
||||||
{
|
{
|
||||||
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
|
Local<Function> domexception = GetDOMException(context).ToLocalChecked();
|
||||||
|
|
@ -1638,6 +1699,7 @@ static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
registry->Register(MessagePort::ReceiveMessage);
|
registry->Register(MessagePort::ReceiveMessage);
|
||||||
registry->Register(MessagePort::MoveToContext);
|
registry->Register(MessagePort::MoveToContext);
|
||||||
registry->Register(SetDeserializerCreateObjectFunction);
|
registry->Register(SetDeserializerCreateObjectFunction);
|
||||||
|
registry->Register(StructuredClone);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,17 @@
|
||||||
// Flags: --expose-internals
|
|
||||||
'use strict';
|
'use strict';
|
||||||
/* eslint-disable no-global-assign */
|
|
||||||
|
|
||||||
require('../common');
|
require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
const {
|
assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' });
|
||||||
structuredClone: _structuredClone,
|
assert.throws(() => structuredClone(undefined, ''), { code: 'ERR_INVALID_ARG_TYPE' });
|
||||||
} = require('internal/structured_clone');
|
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 {
|
// Options can be null or undefined.
|
||||||
strictEqual,
|
assert.strictEqual(structuredClone(undefined), undefined);
|
||||||
throws,
|
assert.strictEqual(structuredClone(undefined, null), undefined);
|
||||||
} = require('assert');
|
// Transfer can be null or undefined.
|
||||||
|
assert.strictEqual(structuredClone(undefined, { transfer: null }), undefined);
|
||||||
strictEqual(globalThis.structuredClone, _structuredClone);
|
assert.strictEqual(structuredClone(undefined, { }), undefined);
|
||||||
structuredClone = undefined;
|
|
||||||
strictEqual(globalThis.structuredClone, undefined);
|
|
||||||
|
|
||||||
// Restore the value for the known globals check.
|
|
||||||
structuredClone = _structuredClone;
|
|
||||||
|
|
||||||
throws(() => _structuredClone(), /ERR_MISSING_ARGS/);
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user