lib: implement webidl dictionary converter and use it in structuredClone

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.

PR-URL: https://github.com/nodejs/node/pull/55489
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
Jason Zhang 2024-10-20 22:37:00 +10:30 committed by Antoine du Hamel
parent 270da88228
commit e53fe03a18
No known key found for this signature in database
GPG Key ID: 21D900FFDB233756
3 changed files with 98 additions and 23 deletions

View File

@ -2,6 +2,7 @@
const {
ArrayPrototypePush,
ArrayPrototypeToSorted,
MathAbs,
MathMax,
MathMin,
@ -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) {
@ -327,6 +389,7 @@ module.exports = {
createEnumConverter,
createInterfaceConverter,
createSequenceConverter,
createDictionaryConverter,
evenRound,
makeException,
};

View File

@ -5,7 +5,6 @@ const {
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_MISSING_ARGS,
},
} = require('internal/errors');
@ -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;

View File

@ -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} cannot 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, ''),
{ 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);