From e53fe03a181f89d205f373b94c818139b52e1943 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Sun, 20 Oct 2024 22:37:00 +1030 Subject: [PATCH] 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 Reviewed-By: Yagiz Nizipli Reviewed-By: Matteo Collina --- lib/internal/webidl.js | 63 ++++++++++++++++++++ lib/internal/worker/js_transferable.js | 37 ++++++------ test/parallel/test-structuredClone-global.js | 21 +++++-- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index e57108b14ba..071e8b9967e 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -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, }; diff --git a/lib/internal/worker/js_transferable.js b/lib/internal/worker/js_transferable.js index 58e377b87a9..592d43d2152 100644 --- a/lib/internal/worker/js_transferable.js +++ b/lib/internal/worker/js_transferable.js @@ -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'], + 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'](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; diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index ef6ddc56a73..e6b63c382b3 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -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);