Skip to content

Commit

Permalink
lib: implement webidl dictionary converter and use it in structuredClone
Browse files Browse the repository at this point in the history
This commit provides a factory to generate `dictionaryConverter`
compliant with the spec. The implemented factory function is used for
the `structuredClone` algorithm with updated test cases.
  • Loading branch information
jazelly committed Oct 26, 2024
1 parent 7934cec commit cfd248f
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 23 deletions.
63 changes: 63 additions & 0 deletions lib/internal/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ArrayPrototypePush,
ArrayPrototypeToSorted,
MathAbs,
MathMax,
MathMin,
Expand Down Expand Up @@ -272,6 +273,67 @@ function type(V) {
}
}

// https://webidl.spec.whatwg.org/#js-dictionary
function createDictionaryConverter(members) {
// The spec requires us to operate the members of a dictionary in
// lexicographical order. We are doing this in the outer scope to
// reduce the overhead that could happen in the returned function.
const sortedMembers = ArrayPrototypeToSorted(members, (a, b) => {
if (a.key === b.key) {
return 0;
}
return a.key < b.key ? -1 : 1;
});

return function(
V,
opts = kEmptyObject,
) {
if (V != null && type(V) !== OBJECT) {
throw makeException(
'cannot be converted to a dictionary',
opts,
);
}

const idlDict = { __proto__: null };
for (let i = 0; i < sortedMembers.length; i++) {
const member = sortedMembers[i];
const key = member.key;
let jsMemberValue;
if (V == null) {
jsMemberValue = undefined;
} else {
jsMemberValue = V[key];
}

if (jsMemberValue !== undefined) {
const memberContext = opts.context ? `${key} in ${opts.context}` : `${key}`;
const converter = member.converter;
const idlMemberValue = converter(
jsMemberValue,
{
__proto__: null,
prefix: opts.prefix,
context: memberContext,
},
);
idlDict[key] = idlMemberValue;
} else if (typeof member.defaultValue === 'function') {
const idlMemberValue = member.defaultValue();
idlDict[key] = idlMemberValue;
} else if (member.required) {
throw makeException(
`cannot be converted because of the missing '${key}'`,
opts,
);
}
}

return idlDict;
};
}

// https://webidl.spec.whatwg.org/#es-sequence
function createSequenceConverter(converter) {
return function(V, opts = kEmptyObject) {
Expand Down Expand Up @@ -327,6 +389,7 @@ module.exports = {
createEnumConverter,
createInterfaceConverter,
createSequenceConverter,
createDictionaryConverter,
evenRound,
makeException,
};
37 changes: 19 additions & 18 deletions lib/internal/worker/js_transferable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const {
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_MISSING_ARGS,
},
} = require('internal/errors');
Expand Down Expand Up @@ -98,29 +97,31 @@ function markTransferMode(obj, cloneable = false, transferable = false) {
obj[transfer_mode_private_symbol] = mode;
}


webidl.converters.StructuredSerializeOptions = webidl
.createDictionaryConverter(
[
{
key: 'transfer',
converter: webidl.converters['sequence<object>'],
defaultValue: () => [],
},
],
);

function structuredClone(value, options) {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('The value argument must be specified');
}

// TODO(jazelly): implement generic webidl dictionary converter
const prefix = 'Options';
const optionsType = webidl.type(options);
if (optionsType !== 'Undefined' && optionsType !== 'Null' && optionsType !== 'Object') {
throw new ERR_INVALID_ARG_TYPE(
prefix,
['object', 'null', 'undefined'],
options,
);
}
const key = 'transfer';
const idlOptions = { __proto__: null, [key]: [] };
if (options != null && key in options && options[key] !== undefined) {
idlOptions[key] = webidl.converters['sequence<object>'](options[key], {
const idlOptions = webidl.converters.StructuredSerializeOptions(
options,
{
__proto__: null,
context: 'Transfer',
});
}
prefix: "Failed to execute 'structuredClone'",
context: 'Options',
},
);

const serializedData = nativeStructuredClone(value, idlOptions);
return serializedData;
Expand Down
21 changes: 16 additions & 5 deletions test/parallel/test-structuredClone-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@
require('../common');
const assert = require('assert');

const prefix = "Failed to execute 'structuredClone'";
const key = 'transfer';
const context = 'Options';
const memberConverterError = `${prefix}: ${key} in ${context} can not be converted to sequence.`;
const dictionaryConverterError = `${prefix}: ${context} can not be converted to a dictionary`;

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' });
assert.throws(() => structuredClone(undefined, { transfer: null }), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => structuredClone(undefined, ''),

Check failure on line 13 in test/parallel/test-structuredClone-global.js

View workflow job for this annotation

GitHub Actions / test-linux

--- stderr --- node:assert:377 throw err; ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected Comparison { code: 'ERR_INVALID_ARG_TYPE', + message: "Failed to execute 'structuredClone': Options cannot be converted to a dictionary" - message: "Failed to execute 'structuredClone': Options can not be converted to a dictionary" } at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:8) at Module._compile (node:internal/modules/cjs/loader:1572:14) at Object..js (node:internal/modules/cjs/loader:1709:10) at Module.load (node:internal/modules/cjs/loader:1315:32) at Function._load (node:internal/modules/cjs/loader:1125:12) at TracingChannel.traceSync (node:diagnostics_channel:322:14) at wrapModuleLoad (node:internal/modules/cjs/loader:216:24) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5) at node:internal/main/run_main_module:36:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: TypeError: Failed to execute 'structuredClone': Options cannot be converted to a dictionary at codedTypeError (node:internal/webidl:216:15) at makeException (node:internal/webidl:225:10) at Object.StructuredSerializeOptions (node:internal/webidl:293:13) at structuredClone (node:internal/worker/js_transferable:117:40) at assert.throws.code (/home/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:21) at getActual (node:assert:498:5) at Function.throws (node:assert:644:24) at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:8) at Module._compile (node:internal/modules/cjs/loader:1572:14) at Object..js (node:internal/modules/cjs/loader:1709:10) { code: 'ERR_INVALID_ARG_TYPE' }, expected: { code: 'ERR_INVALID_ARG_TYPE', message: "Failed to execute 'structuredClone': Options can not be converted to a dictionary" }, operator: 'throws' } Node.js v24.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /home/runner/work/node/node/test/parallel/test-structuredClone-global.js

Check failure on line 13 in test/parallel/test-structuredClone-global.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:assert:377 throw err; ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected Comparison { code: 'ERR_INVALID_ARG_TYPE', + message: "Failed to execute 'structuredClone': Options cannot be converted to a dictionary" - message: "Failed to execute 'structuredClone': Options can not be converted to a dictionary" } at Object.<anonymous> (/Users/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:8) at Module._compile (node:internal/modules/cjs/loader:1572:14) at Object..js (node:internal/modules/cjs/loader:1709:10) at Module.load (node:internal/modules/cjs/loader:1315:32) at Function._load (node:internal/modules/cjs/loader:1125:12) at TracingChannel.traceSync (node:diagnostics_channel:322:14) at wrapModuleLoad (node:internal/modules/cjs/loader:216:24) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5) at node:internal/main/run_main_module:36:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: TypeError: Failed to execute 'structuredClone': Options cannot be converted to a dictionary at codedTypeError (node:internal/webidl:216:15) at makeException (node:internal/webidl:225:10) at Object.StructuredSerializeOptions (node:internal/webidl:293:13) at structuredClone (node:internal/worker/js_transferable:117:40) at assert.throws.code (/Users/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:21) at getActual (node:assert:498:5) at Function.throws (node:assert:644:24) at Object.<anonymous> (/Users/runner/work/node/node/test/parallel/test-structuredClone-global.js:13:8) at Module._compile (node:internal/modules/cjs/loader:1572:14) at Object..js (node:internal/modules/cjs/loader:1709:10) { code: 'ERR_INVALID_ARG_TYPE' }, expected: { code: 'ERR_INVALID_ARG_TYPE', message: "Failed to execute 'structuredClone': Options can not be converted to a dictionary" }, operator: 'throws' } Node.js v24.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /Users/runner/work/node/node/test/parallel/test-structuredClone-global.js
{ code: 'ERR_INVALID_ARG_TYPE', message: dictionaryConverterError });
assert.throws(() => structuredClone(undefined, 1),
{ code: 'ERR_INVALID_ARG_TYPE', message: dictionaryConverterError });
assert.throws(() => structuredClone(undefined, { transfer: 1 }),
{ code: 'ERR_INVALID_ARG_TYPE', message: memberConverterError });
assert.throws(() => structuredClone(undefined, { transfer: '' }),
{ code: 'ERR_INVALID_ARG_TYPE', message: memberConverterError });
assert.throws(() => structuredClone(undefined, { transfer: null }),
{ code: 'ERR_INVALID_ARG_TYPE', message: memberConverterError });

// Options can be null or undefined.
assert.strictEqual(structuredClone(undefined), undefined);
Expand Down

0 comments on commit cfd248f

Please sign in to comment.