Repository: nodejs/node
Pull request: lib: implement webidl dictionary converter and use it in structuredClone

The first commit optimizes `type(V)` in `webidl.js` to return enum
instead of string.

The second commit provides a factory to generate `dictionaryConverter`
compliant with the spec. The implemented factory function is used for
the `structuredClone` algorithm, along with updated test cases.

Changed files:
filename: lib/internal/webidl.js
patch: @@ -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,
 };

filename: lib/internal/worker/js_transferable.js
patch: @@ -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;

filename: test/parallel/test-structuredClone-global.js
patch: @@ -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);