Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: implement structuredClone in native #50330

Merged
merged 4 commits into from Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 46 additions & 0 deletions benchmark/misc/structured-clone.js
@@ -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);
}
2 changes: 0 additions & 2 deletions lib/.eslintrc.yaml
Expand Up @@ -171,8 +171,6 @@ rules:
message: Use `const { setInterval } = require('timers');` instead of the global.
- 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.
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
- name: SubtleCrypto
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
message: Use `const { SubtleCrypto } = require('internal/crypto/webcrypto');` instead of the global.
no-restricted-modules:
Expand Down
7 changes: 2 additions & 5 deletions lib/internal/bootstrap/web/exposed-window-or-worker.js
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/perf/usertiming.js
Expand Up @@ -31,7 +31,7 @@ const {
},
} = require('internal/errors');

const { structuredClone } = require('internal/structured_clone');
const { structuredClone } = internalBinding('messaging');
const {
lazyDOMException,
kEnumerableProperty,
Expand Down
29 changes: 0 additions & 29 deletions lib/internal/structured_clone.js

This file was deleted.

4 changes: 1 addition & 3 deletions lib/internal/webstreams/readablestream.js
Expand Up @@ -85,9 +85,7 @@ const {
kControllerErrorFunction,
} = require('internal/streams/utils');

const {
structuredClone,
} = require('internal/structured_clone');
const { structuredClone } = internalBinding('messaging');

const {
ArrayBufferViewGetBuffer,
Expand Down
112 changes: 87 additions & 25 deletions src/node_messaging.cc
Expand Up @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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");
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}
Local<Object> options = args[1].As<Object>();
Local<Value> transfer_list_v;
if (!options->Get(context, FIXED_ONE_BYTE_STRING(isolate, "transfer"))
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
.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()) {
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
30 changes: 12 additions & 18 deletions test/parallel/test-structuredClone-global.js
@@ -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);