current commit
260929.14 · worker: fix postMessage overload resolution
'use strict';

const {
  ArrayBufferIsView,
  ArrayBufferPrototypeGetResizable,
  ArrayIsArray,
  ArrayPrototypePush,
  ArrayPrototypeToSorted,
  BigInt,
  DataViewPrototypeGetBuffer,
  FunctionPrototypeCall,
  MathAbs,
  MathMax,
  MathMin,
  MathPow,
  MathSign,
  MathTrunc,
  Number,
  NumberIsFinite,
  NumberIsNaN,
  NumberMAX_SAFE_INTEGER,
  NumberMIN_SAFE_INTEGER,
  ObjectPrototypeHasOwnProperty,
  SafeArrayIterator,
  SafeSet,
  String,
  StringPrototypeToWellFormed,
  SymbolIterator,
  TypeError,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetSymbolToStringTag,
} = primordials;

const { kEmptyObject, setOwnProperty } = require('internal/util');
const {
  isSharedArrayBuffer,
  isTypedArray,
} = require('internal/util/types');

const { getSharedArrayBufferGrowable } = internalBinding('util');

const BIGINT_2_63 = 1n << 63n;
const BIGINT_2_64 = 1n << 64n;

const converters = { __proto__: null };

/**
 * @typedef {object} ConversionOptions
 * @property {string} [prefix] Message prefix for operation failures.
 * @property {string} [context] Message context for the converted value.
 * @property {string} [code] Node.js error code to assign to TypeError.
 * @property {boolean} [enforceRange] Web IDL [EnforceRange] attribute.
 * @property {boolean} [clamp] Web IDL [Clamp] attribute.
 * @property {boolean} [allowShared] Web IDL [AllowShared] attribute for
 *   buffer view types.
 * @property {boolean} [allowResizable] Web IDL [AllowResizable] attribute.
 */

/**
 * @callback Converter
 * @param {any} V JavaScript value to convert to an IDL value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {any}
 */

/**
 * @callback DictionaryMemberValidator
 * @param {any} idlMemberValue Converted IDL member value.
 * @param {object} jsDict Original JavaScript dictionary object.
 * @returns {void}
 */

/**
 * @typedef {object} DictionaryMember
 * @property {string} key Dictionary member identifier.
 * @property {Converter} converter Converter for the member type.
 * @property {boolean} [required] Whether the member is required.
 * @property {() => any} [defaultValue] Function returning the default value.
 * @property {DictionaryMemberValidator} [validator] Optional Node.js
 *   extension point invoked after conversion and before storing the member.
 *   This is for early semantic validation of known unsupported IDL values,
 *   especially in Web Crypto, where SubtleCrypto.supports() needs to answer
 *   from normalized dictionaries without running the requested operation.
 */

/**
 * Creates a TypeError with a Node.js error code.
 * @param {string} message Error message.
 * @param {string} code Node.js error code to assign.
 * @returns {TypeError}
 */
function codedTypeError(message, code) {
  // eslint-disable-next-line no-restricted-syntax
  const err = new TypeError(message);
  setOwnProperty(err, 'code', code);
  return err;
}

/**
 * Creates the exception thrown by Web IDL converters.
 * @param {string} message Unprefixed conversion failure message.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {TypeError}
 */
function makeException(message, options = kEmptyObject) {
  const prefix = options.prefix ? options.prefix + ': ' : '';
  const context = options.context?.length === 0 ?
    '' : (options.context ?? 'Value') + ' ';
  return codedTypeError(
    `${prefix}${context}${message}`,
    options.code || 'ERR_INVALID_ARG_TYPE',
  );
}

/**
 * Builds derived conversion options for nested converter calls and adjusted
 * error codes. These objects are allocated on dictionary/sequence conversion
 * hot paths, so keep their shape stable and avoid object spread and
 * null-prototype objects.
 * @param {ConversionOptions} options Parent conversion options.
 * @param {string} [context] Replacement context.
 * @param {string} [code] Replacement error code.
 * @returns {ConversionOptions}
 */
function makeOptions(options, context = options.context, code = options.code) {
  return {
    prefix: options.prefix,
    context,
    code,
    enforceRange: options.enforceRange,
    clamp: options.clamp,
    allowShared: options.allowShared,
    allowResizable: options.allowResizable,
  };
}

/**
 * Returns the ECMAScript specification type of a JavaScript value.
 * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
 * @param {any} V JavaScript value.
 * @returns {'Undefined'|'Null'|'Boolean'|'String'|'Symbol'|'Number'|'BigInt'|'Object'}
 */
function type(V) {
  // ECMA-262 6.1: map JavaScript values to language type names.
  switch (typeof V) {
    case 'undefined':
      return 'Undefined';
    case 'boolean':
      return 'Boolean';
    case 'string':
      return 'String';
    case 'symbol':
      return 'Symbol';
    case 'number':
      return 'Number';
    case 'bigint':
      return 'BigInt';
    case 'object':
    case 'function':
    default:
      // ECMA-262 6.1.2: null is its own language type.
      // ECMA-262 6.1.7: functions are Object values.
      if (V === null) {
        return 'Null';
      }
      return 'Object';
  }
}

/**
 * Returns IntegerPart(n).
 * @see https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
 * @param {number} n Numeric value.
 * @returns {number}
 */
function integerPart(n) {
  // Web IDL IntegerPart steps 1-3: floor(abs(n)), restore the sign,
  // and choose +0 rather than -0.
  const integer = MathTrunc(n);
  return integer === 0 ? 0 : integer;
}

/**
 * Rounds to the nearest integer, choosing the even integer on ties.
 * @param {number} x Numeric value.
 * @returns {number}
 */
function evenRound(x) {
  // Web IDL ConvertToInt step 7.2: round to the nearest integer,
  // choosing the even integer on ties and +0 rather than -0.
  const i = integerPart(x);
  const remainder = MathAbs(x % 1);
  const sign = MathSign(x);
  if (remainder === 0.5) {
    return i % 2 === 0 ? i : i + sign;
  }
  const r = remainder < 0.5 ? i : i + sign;
  if (r === 0) {
    return 0;
  }
  return r;
}

/**
 * Returns 2 to the power of the given exponent.
 * @param {number} exponent Non-negative integer exponent.
 * @returns {number}
 */
function pow2(exponent) {
  if (exponent < 31) {
    return 1 << exponent;
  }
  if (exponent === 31) {
    return 0x8000_0000;
  }
  if (exponent === 32) {
    return 0x1_0000_0000;
  }
  return MathPow(2, exponent);
}

/**
 * Returns x modulo y for Web IDL ConvertToInt step 10.
 *
 * This is intentionally not a general modulo helper. ConvertToInt only calls
 * it with a positive power-of-two modulus, and the implementation assumes
 * that. It converts JavaScript remainder into mathematical modulo and
 * normalizes -0 to +0.
 * @param {number} x Dividend.
 * @param {number} y Positive divisor.
 * @returns {number}
 */
function modulo(x, y) {
  // Web IDL ConvertToInt step 10 uses mathematical modulo.
  const r = x % y;
  if (r === 0) {
    return 0;
  }
  return r > 0 ? r : r + y;
}

/**
 * Returns x modulo y for Web IDL ConvertToInt step 10.
 *
 * This is intentionally not a general modulo helper. ConvertToInt only calls
 * it with a positive power-of-two modulus, and the implementation assumes
 * that. BigInt has no -0, but this mirrors modulo()'s mathematical modulo
 * behavior for the 64-bit path.
 * @param {bigint} x Dividend.
 * @param {bigint} y Positive divisor.
 * @returns {bigint}
 */
function bigIntModulo(x, y) {
  // Web IDL ConvertToInt step 10 uses mathematical modulo.
  const r = x % y;
  return r >= 0n ? r : r + y;
}

/**
 * Returns ToNumber(V).
 * @see https://tc39.es/ecma262/#sec-tonumber
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {number}
 */
function toNumber(V, options = kEmptyObject) {
  if (typeof V === 'bigint') {
    // ECMA-262 ToNumber step 2: BigInt values throw.
    throw makeException(
      'is a BigInt and cannot be converted to a number.',
      options);
  }
  if (typeof V === 'symbol') {
    // ECMA-262 ToNumber step 2: Symbol values throw.
    throw makeException(
      'is a Symbol and cannot be converted to a number.',
      options);
  }

  // Unary plus performs ToNumber, including ToPrimitive(V, number) for
  // objects. Number(V) is not equivalent because it converts BigInt values,
  // including BigInt values produced by ToPrimitive.
  // Abrupt completions and native TypeErrors propagate unchanged. This is an
  // intentional diagnostics tradeoff: decorating object conversion failures
  // would require maintaining local ECMA-262 ToPrimitive and
  // OrdinaryToPrimitive implementations.
  return +V;
}

/**
 * Returns ToString(V).
 * @see https://tc39.es/ecma262/#sec-tostring
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {string}
 */
function toString(V, options = kEmptyObject) {
  if (typeof V === 'symbol') {
    // ECMA-262 ToString step 2: Symbol values throw.
    throw makeException(
      'is a Symbol and cannot be converted to a string.',
      options);
  }

  // The String function performs ToString for all non-Symbol primitives and
  // objects, including ToPrimitive(V, string). String concatenation is not
  // equivalent because it uses ToPrimitive(V, default). Abrupt completions
  // and native TypeErrors propagate unchanged. This is an intentional
  // diagnostics tradeoff: decorating object conversion failures would require
  // maintaining local ECMA-262 ToPrimitive and OrdinaryToPrimitive
  // implementations.
  return String(V);
}

/**
 * Converts a JavaScript value to a Web IDL integer value.
 * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
 * @param {any} V JavaScript value.
 * @param {number} bitLength Integer bit length.
 * @param {'signed'|'unsigned'} [signedness] Integer signedness.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {number}
 */
function convertToInt(
  V,
  bitLength,
  signedness = 'unsigned',
  options = kEmptyObject,
) {
  const signed = signedness === 'signed';
  let upperBound;
  let lowerBound;

  // Web IDL ConvertToInt steps 1-3: determine lower/upper bounds.
  if (bitLength === 64) {
    // Steps 1.1-1.3 set upperBound to 2^53 - 1 and lowerBound to 0
    // for unsigned, or -2^53 + 1 for signed. This ensures 64-bit
    // integer types associated with [EnforceRange] or [Clamp] are
    // representable in JavaScript's Number type as unambiguous integers.
    upperBound = NumberMAX_SAFE_INTEGER;
    lowerBound = signed ? NumberMIN_SAFE_INTEGER : 0;
  } else if (!signed) {
    // Spell out the common Web IDL integer sizes so hot converters avoid
    // recomputing powers of two on every call.
    lowerBound = 0;
    if (bitLength === 8) {
      upperBound = 0xff;
    } else if (bitLength === 16) {
      upperBound = 0xffff;
    } else if (bitLength === 32) {
      upperBound = 0xffff_ffff;
    } else {
      upperBound = pow2(bitLength) - 1;
    }
  } else if (bitLength === 8) {
    // Signed 8/16/32-bit conversions are mostly exercised through direct
    // convertToInt() calls, but keep their common bounds cheap too.
    lowerBound = -0x80;
    upperBound = 0x7f;
  } else if (bitLength === 16) {
    lowerBound = -0x8000;
    upperBound = 0x7fff;
  } else if (bitLength === 32) {
    lowerBound = -0x8000_0000;
    upperBound = 0x7fff_ffff;
  } else {
    lowerBound = -pow2(bitLength - 1);
    upperBound = pow2(bitLength - 1) - 1;
  }

  // Common case: primitive Number values that already fit the Web IDL
  // range and have no fractional part are returned unchanged by every
  // ConvertToInt path, except that -0 must become +0. This skips the
  // generic ToNumber and option handling without skipping observable
  // object coercion.
  let x;
  if (typeof V === 'number') {
    // For primitive Numbers, in-range non-[Clamp] conversion is either
    // identity or IntegerPart(V). This keeps the default and [EnforceRange]
    // paths out of the generic ToNumber/options flow.
    if (V >= lowerBound && V <= upperBound) {
      const integer = MathTrunc(V);
      if (integer === V) {
        return V === 0 ? 0 : V;
      }
      if (options !== kEmptyObject && options.clamp) {
        return evenRound(V);
      }
      return integer === 0 ? 0 : integer;
    }
    if (options !== kEmptyObject && options.enforceRange) {
      // Keep [EnforceRange] ahead of [Clamp] without falling through to
      // the shared check, which would observe options.enforceRange again.
      if (!NumberIsFinite(V)) {
        throw makeException(
          'is not a finite number.',
          options);
      }

      const integer = integerPart(V);
      if (integer < lowerBound || integer > upperBound) {
        throw makeException(
          `is outside the expected range of ${lowerBound} to ${upperBound}.`,
          makeOptions(options, options.context, 'ERR_OUT_OF_RANGE'));
      }

      return integer;
    }
    if (options !== kEmptyObject && options.clamp && !NumberIsNaN(V)) {
      // Out-of-range [Clamp] returns one of the already-computed bounds.
      if (V <= lowerBound) {
        return lowerBound === 0 ? 0 : lowerBound;
      }
      if (V >= upperBound) {
        return upperBound === 0 ? 0 : upperBound;
      }
    }
    x = V;
  } else {
    // Step 4: convert V with ECMA-262 ToNumber.
    x = toNumber(V, options);
  }
  // Step 5: normalize -0 to +0.
  if (x === 0) {
    x = 0;
  }

  // Step 6: [EnforceRange] rejects non-finite and out-of-range values.
  if (options.enforceRange) {
    // Step 6.1: reject NaN and infinities.
    if (!NumberIsFinite(x)) {
      throw makeException(
        'is not a finite number.',
        options);
    }

    // Step 6.2: truncate to IntegerPart(x).
    x = integerPart(x);

    // Steps 6.3-6.4: reject out-of-range values, otherwise return.
    if (x < lowerBound || x > upperBound) {
      throw makeException(
        `is outside the expected range of ${lowerBound} to ${upperBound}.`,
        makeOptions(options, options.context, 'ERR_OUT_OF_RANGE'));
    }

    return x;
  }

  // Step 7: [Clamp] clamps, rounds, and returns non-NaN values.
  if (options.clamp && !NumberIsNaN(x)) {
    // Step 7.1: clamp x into the supported bounds.
    x = MathMin(MathMax(x, lowerBound), upperBound);
    // Steps 7.2-7.3: round ties to even and return.
    return evenRound(x);
  }

  // Step 8: NaN, +0, -0, and infinities become +0.
  if (!NumberIsFinite(x) || x === 0) {
    return 0;
  }

  // Step 9: truncate to IntegerPart(x).
  x = integerPart(x);

  // Steps 10-12 are an identity for values already in the step 1-3
  // bounds. For 64-bit conversions this only skips the safe-integer
  // subset; values outside it still need exact BigInt modulo and the
  // final Number approximation.
  if (x >= lowerBound && x <= upperBound) {
    return x;
  }

  if (bitLength === 64) {
    // Steps 10-12 still wrap over the full 2^64 IDL integer range.
    // BigInt keeps x modulo 2^64 and the signed high-bit adjustment exact
    // before this helper returns the JavaScript binding result.
    let xBigInt = BigInt(x);
    xBigInt = bigIntModulo(xBigInt, BIGINT_2_64);

    // For long long and unsigned long long values outside the safe-integer
    // range, Web IDL says the JS Number value represents the closest numeric
    // value, choosing the value with an even significand if there are two
    // equally close values. Number(BigInt) performs that final approximation.

    // Step 11: wrap into the signed range when the high bit is set.
    if (signed && xBigInt >= BIGINT_2_63) {
      return Number(xBigInt - BIGINT_2_64);
    }

    // Step 12: return the unsigned value.
    return Number(xBigInt);
  }

  // For 8/16/32-bit conversions, bitwise operators perform the same
  // power-of-two wrapping as Web IDL step 10 for finite integer Numbers.
  // The shifts narrow the unsigned value into the signed range when needed.
  if (bitLength === 8) {
    return signed ? (x << 24) >> 24 : x & 0xff;
  }
  if (bitLength === 16) {
    return signed ? (x << 16) >> 16 : x & 0xffff;
  }
  if (bitLength === 32) {
    return signed ? x | 0 : x >>> 0;
  }

  // Step 10: reduce modulo 2^bitLength.
  const twoToTheBitLength = pow2(bitLength);
  x = modulo(x, twoToTheBitLength);

  // Step 11: wrap into the signed range when the high bit is set.
  if (signed && x >= pow2(bitLength - 1)) {
    return x - twoToTheBitLength;
  }

  // Step 12: return the unsigned value.
  return x;
}

/**
 * Creates a converter for a Web IDL integer type.
 * @param {number} bitLength Integer bit length.
 * @param {'signed'|'unsigned'} [signedness] Integer signedness.
 * @returns {Converter}
 */
function createIntegerConverter(bitLength, signedness = 'unsigned') {
  return (V, options = kEmptyObject) => {
    // Integer conversion step 1 calls ConvertToInt; step 2 returns
    // the IDL value with the same numeric value.
    return convertToInt(V, bitLength, signedness, options);
  };
}

/**
 * Converts a JavaScript value to the IDL boolean type.
 * @see https://webidl.spec.whatwg.org/#es-boolean
 * @param {any} V JavaScript value.
 * @returns {boolean}
 */
converters.boolean = (V) => {
  // Web IDL boolean steps 1-2: ToBoolean(V), then return the same
  // truth value as an IDL boolean.
  return !!V;
};

/**
 * Converts a JavaScript value to the IDL object type.
 * @see https://webidl.spec.whatwg.org/#es-object
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {object|Function}
 */
converters.object = (V, options = kEmptyObject) => {
  // Web IDL object step 1: throw unless V is an ECMA-262 Object.
  if (type(V) !== 'Object') {
    throw makeException(
      'is not an object.',
      options,
    );
  }
  // Step 2: return a reference to the same object.
  return V;
};

/**
 * Converts a JavaScript value to the IDL octet type.
 * @see https://webidl.spec.whatwg.org/#es-octet
 * @type {Converter}
 */
converters.octet = createIntegerConverter(8);

/**
 * Converts a JavaScript value to the IDL unsigned short type.
 * @see https://webidl.spec.whatwg.org/#es-unsigned-short
 * @type {Converter}
 */
converters['unsigned short'] = createIntegerConverter(16);

/**
 * Converts a JavaScript value to the IDL unsigned long type.
 * @see https://webidl.spec.whatwg.org/#es-unsigned-long
 * @type {Converter}
 */
converters['unsigned long'] = createIntegerConverter(32);

/**
 * Converts a JavaScript value to the IDL long long type.
 * @see https://webidl.spec.whatwg.org/#es-long-long
 * @type {Converter}
 */
converters['long long'] = createIntegerConverter(64, 'signed');

/**
 * Converts a JavaScript value to the IDL DOMString type.
 * @see https://webidl.spec.whatwg.org/#es-DOMString
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {string}
 */
converters.DOMString = function DOMString(V, options = kEmptyObject) {
  // Step 1 only applies to [LegacyNullToEmptyString], which this core
  // converter does not implement. Steps 2-3 apply ToString(V) and
  // return a DOMString with the same code units.
  return toString(V, options);
};

/**
 * Converts a JavaScript value to the IDL USVString type.
 * @see https://webidl.spec.whatwg.org/#es-USVString
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {string}
 */
converters.USVString = function USVString(V, options = kEmptyObject) {
  return StringPrototypeToWellFormed(converters.DOMString(V, options));
};

/**
 * Throws when a Web IDL operation receives too few arguments.
 * @param {number} length Actual argument count.
 * @param {number} required Required argument count.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {void}
 */
function requiredArguments(length, required, options = kEmptyObject) {
  if (length < required) {
    throw makeException(
      `${required} argument${
        required === 1 ? '' : 's'
      } required, but only ${length} present.`,
      makeOptions(options, '', 'ERR_MISSING_ARGS'));
  }
}

/**
 * Creates a converter for a Web IDL enum type.
 * @see https://webidl.spec.whatwg.org/#es-enumeration
 * @param {string} name Enum identifier.
 * @param {string[]} values Enum values.
 * @returns {Converter}
 */
function createEnumConverter(name, values) {
  const E = new SafeSet(new SafeArrayIterator(values));

  return function(V, options = kEmptyObject) {
    // Web IDL enumeration step 1: convert V with ToString.
    const S = toString(V, options);

    // Step 2: throw unless S is one of the enumeration values.
    if (!E.has(S)) {
      throw makeException(
        `'${S}' is not a valid enum value of type ${name}.`,
        makeOptions(options, options.context, 'ERR_INVALID_ARG_VALUE'));
    }

    // Step 3: return the matching enumeration value.
    return S;
  };
}

/**
 * Returns the context used when converting a dictionary member.
 * @param {string} key Dictionary member identifier.
 * @param {ConversionOptions} options Conversion options.
 * @returns {string}
 */
function dictionaryMemberContext(key, options) {
  return options.context ? `${key} in ${options.context}` : key;
}

/**
 * Returns the context used when converting a sequence element.
 * @param {number} index Sequence element index.
 * @param {ConversionOptions} options Conversion options.
 * @returns {string}
 */
function sequenceElementContext(index, options) {
  return `${options.context ?? 'Value'}[${index}]`;
}

/**
 * Returns the message used for a missing required dictionary member.
 * @param {string} dictionaryName Dictionary identifier.
 * @param {string} key Dictionary member identifier.
 * @returns {string}
 */
function missingDictionaryMemberMessage(dictionaryName, key) {
  return `cannot be converted to '${dictionaryName}' because ` +
    `'${key}' is required in '${dictionaryName}'.`;
}

/**
 * Creates a converter for a Web IDL dictionary type.
 * @see https://webidl.spec.whatwg.org/#js-dictionary
 * @param {string} dictionaryName Dictionary identifier.
 * @param {DictionaryMember[]|DictionaryMember[][]} members Dictionary members,
 *   either for a single dictionary or grouped from least-derived to
 *   most-derived dictionary.
 * @returns {Converter}
 */
function createDictionaryConverter(
  dictionaryName,
  members,
) {
  const compareMembers = (a, b) => {
    if (a.key === b.key) {
      return 0;
    }
    return a.key < b.key ? -1 : 1;
  };

  const dictionaries = ArrayIsArray(members[0]) ? members : [members];
  const sortedMembers = [];

  function ownMember(member, key) {
    return ObjectPrototypeHasOwnProperty(member, key) ? member[key] : undefined;
  }

  // Dictionaries with no defaults and no required members skip steps
  // 4.1.5/4.1.6 entirely, keeping the absent-member path free.
  let anyMissingMemberHandling = false;

  // Web IDL dictionary conversion steps 3-4 process inherited dictionaries
  // from least-derived to most-derived and sort only within each dictionary.
  // Callers with inheritance pass one member array per dictionary level.
  for (let i = 0; i < dictionaries.length; i++) {
    const sortedDictionary = ArrayPrototypeToSorted(
      dictionaries[i],
      compareMembers,
    );
    // Definition sites spell out only the members they need, so reading the
    // optional ones below would resolve through %Object.prototype%.
    // Re-materialize each descriptor once with every key present, copied from
    // own properties only. The ordinary prototype is deliberate: nothing
    // consults it now, and detaching it measurably slows these reads down.
    for (let j = 0; j < sortedDictionary.length; j++) {
      const member = sortedDictionary[j];
      const defaultValue = ownMember(member, 'defaultValue');
      const required = ownMember(member, 'required');
      if (typeof defaultValue === 'function' || required) {
        anyMissingMemberHandling = true;
      }
      setOwnProperty(sortedMembers, sortedMembers.length, {
        key: ownMember(member, 'key'),
        converter: ownMember(member, 'converter'),
        defaultValue,
        required,
        validator: ownMember(member, 'validator'),
      });
    }
  }

  const hasMissingMemberHandling = anyMissingMemberHandling;

  return function(jsDict, options = kEmptyObject) {
    // Step 1: reject non-object, non-null, non-undefined values.
    if (jsDict != null && type(jsDict) !== 'Object') {
      throw makeException(
        'cannot be converted to a dictionary',
        options,
      );
    }

    // Step 2: create the IDL dictionary value.
    const idlDict = { __proto__: null };
    if (jsDict == null && !hasMissingMemberHandling) return idlDict;

    // Steps 3-4: members retain their per-dictionary order from construction.
    for (let i = 0; i < sortedMembers.length; i++) {
      const member = sortedMembers[i];
      // Step 4.1.1: get the dictionary member identifier.
      const key = member.key;
      // Steps 4.1.2-4.1.3: read the JavaScript member value.
      const jsMemberValue = jsDict == null ? undefined : jsDict[key];

      // Step 4.1.4: convert and store present member values.
      if (jsMemberValue !== undefined) {
        const converter = member.converter;
        // Step 4.1.4.1: convert the JavaScript value to IDL.
        const idlMemberValue = converter(
          jsMemberValue,
          makeOptions(options, dictionaryMemberContext(key, options)),
        );
        // Validators are a Node.js extension after conversion. They let
        // consumers reject known unsupported values while dictionary
        // conversion still has precise member context. Web Crypto uses this
        // so SubtleCrypto.supports() can make accurate decisions from
        // normalized dictionaries instead of probing by running operations.
        member.validator?.(idlMemberValue, jsDict);
        // Step 4.1.4.2: set idlDict[key] to the IDL value.
        idlDict[key] = idlMemberValue;
      } else if (hasMissingMemberHandling) {
        if (typeof member.defaultValue === 'function') {
          // Step 4.1.5: store the member default value.
          idlDict[key] = member.defaultValue();
        } else if (member.required) {
          // Step 4.1.6: required missing members throw.
          throw makeException(
            missingDictionaryMemberMessage(dictionaryName, key),
            makeOptions(options, options.context, 'ERR_MISSING_OPTION'));
        }
      }
    }

    // Step 5: return the IDL dictionary.
    return idlDict;
  };
}

/**
 * Creates a converter for a Web IDL sequence type.
 * @see https://webidl.spec.whatwg.org/#es-sequence
 * @param {Converter} converter Element converter.
 * @returns {Converter}
 */
function createSequenceConverter(converter) {
  return function(V, options = kEmptyObject, method = undefined) {
    // Web IDL sequence conversion step 1: require an ECMA-262 Object.
    if (type(V) !== 'Object') {
      throw makeException(
        'cannot be converted to sequence.',
        options);
    }

    // Step 2: GetMethod(V, %Symbol.iterator%). Overload resolution can
    // supply the method it already retrieved, avoiding a second lookup.
    if (method === undefined) {
      method = V[SymbolIterator];
    }
    // Step 3: throw if the iterator method is undefined, null, or not callable.
    if (typeof method !== 'function') {
      throw makeException(
        'cannot be converted to sequence.',
        options);
    }

    // Step 4 and create-sequence step 1: get the iterator record.
    const iterator = FunctionPrototypeCall(method, V);
    if (type(iterator) !== 'Object') {
      throw makeException(
        'cannot be converted to sequence.',
        options);
    }
    const nextMethod = iterator.next;
    if (typeof nextMethod !== 'function') {
      throw makeException(
        'cannot be converted to sequence.',
        options);
    }

    // Create-sequence step 2: initialize i to 0.
    const idlSequence = [];
    while (true) {
      // Step 3.1: IteratorStepValue(iteratorRecord).
      const next = FunctionPrototypeCall(nextMethod, iterator);
      if (type(next) !== 'Object') {
        throw makeException(
          'cannot be converted to sequence.',
          options);
      }
      // Step 3.2: IteratorComplete applies ToBoolean(done).
      if (next.done) {
        break;
      }
      // Step 3.3: convert next to an IDL value of type T.
      const idlValue = converter(
        next.value,
        makeOptions(options, sequenceElementContext(idlSequence.length, options)),
      );
      // Step 3.4: store the value and advance i.
      ArrayPrototypePush(idlSequence, idlValue);
    }
    return idlSequence;
  };
}

/**
 * Creates a converter for a Web IDL interface type.
 * @see https://webidl.spec.whatwg.org/#js-interface
 * @param {string} name Interface identifier.
 * @param {(value: any) => boolean} brandCheck Interface brand predicate.
 * @returns {Converter}
 */
function createInterfaceConverter(name, brandCheck) {
  return (V, options = kEmptyObject) => {
    // Web IDL interface conversion step 1: return V if it implements I.
    if (brandCheck(V)) {
      return V;
    }
    // Step 2: otherwise throw.
    throw makeException(
      `is not of type ${name}.`,
      options);
  };
}

/**
 * Returns the ArrayBuffer or SharedArrayBuffer viewed by a typed array or
 * DataView.
 * @param {ArrayBufferView} V TypedArray or DataView.
 * @returns {ArrayBuffer|SharedArrayBuffer}
 */
function getViewedArrayBuffer(V) {
  // Buffer view conversion steps read V.[[ViewedArrayBuffer]].
  return isTypedArray(V) ?
    TypedArrayPrototypeGetBuffer(V) : DataViewPrototypeGetBuffer(V);
}

converters.ArrayBufferView = (V, options = kEmptyObject) => {
  if (!ArrayBufferIsView(V)) {
    throw makeException('is not an ArrayBufferView.', options);
  }
  validateBufferSourceBacking(getViewedArrayBuffer(V), options);
  return V;
};

/**
 * Validates [AllowShared] and [AllowResizable] backing-store constraints.
 * @param {ArrayBuffer|SharedArrayBuffer} buffer Backing buffer.
 * @param {ConversionOptions} options Conversion options.
 * @returns {void}
 */
function validateBufferSourceBacking(buffer, options) {
  let resizable;
  try {
    // ArrayBuffer.prototype.resizable is an ArrayBuffer brand check and the
    // [AllowResizable] value we need. For SharedArrayBuffer-backed views it
    // throws, which lets this path avoid a separate IsSharedArrayBuffer check.
    // BufferSource has separate inline logic because [AllowShared] cannot be
    // used with that typedef.
    resizable = ArrayBufferPrototypeGetResizable(buffer);
  } catch {
    // ArrayBufferView conversion step 2: reject SharedArrayBuffer
    // backing stores unless [AllowShared] is present.
    if (!options.allowShared) {
      throw makeException(
        'is a view on a SharedArrayBuffer, which is not allowed.',
        options);
    }
    // Step 3: reject non-fixed SharedArrayBuffer backing stores unless
    // [AllowResizable] is present.
    validateAllowGrowableSharedArrayBuffer(buffer, options);
    return;
  }

  // ArrayBuffer conversion step 3 and ArrayBufferView conversion step 3:
  // reject non-fixed ArrayBuffer backing stores unless [AllowResizable]
  // is present.
  validateAllowResizableArrayBuffer(resizable, options);
}

/**
 * Validates the [AllowResizable] constraint for growable SharedArrayBuffer.
 * @param {SharedArrayBuffer} buffer SharedArrayBuffer backing buffer.
 * @param {ConversionOptions} options Conversion options.
 * @returns {void}
 */
function validateAllowGrowableSharedArrayBuffer(buffer, options) {
  // SharedArrayBuffer and ArrayBufferView conversion step 3:
  // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable].
  if (!options.allowResizable &&
      FunctionPrototypeCall(getSharedArrayBufferGrowable, buffer)) {
    throw makeException(
      'is backed by a growable SharedArrayBuffer, which is not allowed.',
      options);
  }
}

/**
 * Validates the [AllowResizable] constraint for resizable ArrayBuffer.
 * @param {boolean} resizable ArrayBuffer [[ArrayBufferResizable]] value.
 * @param {ConversionOptions} options Conversion options.
 * @returns {void}
 */
function validateAllowResizableArrayBuffer(resizable, options) {
  // ArrayBuffer and ArrayBufferView conversion step 3:
  // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable].
  // Read [[ArrayBufferResizable]] first so fixed buffers skip the options
  // property lookup on this hot path.
  if (resizable && !options.allowResizable) {
    throw makeException(
      'is backed by a resizable ArrayBuffer, which is not allowed.',
      options);
  }
}

/**
 * Converts a JavaScript value to the IDL Uint8Array type.
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {Uint8Array}
 */
converters.Uint8Array = (V, options = kEmptyObject) => {
  // Typed array conversion steps 1-2: T is Uint8Array, and V must
  // have [[TypedArrayName]] equal to "Uint8Array".
  if (!ArrayBufferIsView(V) ||
    TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') {
    throw makeException(
      'is not an Uint8Array object.',
      options);
  }
  // Steps 3-4: validate [AllowShared] and [AllowResizable].
  validateBufferSourceBacking(TypedArrayPrototypeGetBuffer(V), options);

  // Step 5: return a reference to the same object.
  return V;
};

/**
 * Converts a JavaScript value to the IDL BufferSource typedef.
 * @see https://webidl.spec.whatwg.org/#BufferSource
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {ArrayBuffer|ArrayBufferView<ArrayBuffer>}
 */
converters.BufferSource = (V, options = kEmptyObject) => {
  // BufferSource is a typedef for (ArrayBufferView or ArrayBuffer).
  // [AllowShared] cannot be used with BufferSource because the ArrayBuffer
  // union branch does not support it. Use AllowSharedBufferSource instead.
  if (ArrayBufferIsView(V)) {
    const buffer = getViewedArrayBuffer(V);
    // ArrayBufferView conversion steps 2-4: validate the viewed buffer
    // and return a reference to the same view.
    // Keep this logic inline instead of calling validateBufferSourceBacking().
    // BufferSource conversion is hot, and this avoids a helper call while still
    // using a primordial getter for the backing-buffer internal-slot check.
    // Unlike validateBufferSourceBacking(), this intentionally ignores
    // options.allowShared because [AllowShared] cannot be used with
    // BufferSource.
    let resizable;
    try {
      // ArrayBuffer.prototype.resizable is both the ArrayBuffer brand check
      // and the step 3 value. It throws for SharedArrayBuffer, which lets this
      // path reject SAB-backed views without an extra byteLength getter call.
      resizable = ArrayBufferPrototypeGetResizable(buffer);
    } catch {
      throw makeException(
        'is a view on a SharedArrayBuffer, which is not allowed.',
        options);
    }
    if (resizable && !options.allowResizable) {
      throw makeException(
        'is backed by a resizable ArrayBuffer, which is not allowed.',
        options);
    }
    return V;
  }

  // ArrayBuffer conversion steps 1-2: require a non-shared ArrayBuffer.
  // Use the primordial resizable getter as both the ArrayBuffer brand check
  // and the step 3 value. This avoids isArrayBuffer(V) followed by another
  // getter call, and rejects SharedArrayBuffer on this union branch.
  let resizable;
  try {
    resizable = ArrayBufferPrototypeGetResizable(V);
  } catch {
    throw makeException(
      'is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.',
      options);
  }

  // ArrayBuffer conversion step 3: validate [AllowResizable].
  // ArrayBufferPrototypeGetResizable(V) already excluded SharedArrayBuffer,
  // so no [AllowShared] validation is needed on this branch.
  if (resizable && !options.allowResizable) {
    throw makeException(
      'is backed by a resizable ArrayBuffer, which is not allowed.',
      options);
  }

  // Step 4: return a reference to the same ArrayBuffer.
  return V;
};

/**
 * Converts a JavaScript value to the IDL AllowSharedBufferSource typedef.
 * @see https://webidl.spec.whatwg.org/#AllowSharedBufferSource
 * @param {any} V JavaScript value.
 * @param {ConversionOptions} [options] Conversion options.
 * @returns {ArrayBuffer|SharedArrayBuffer|ArrayBufferView}
 */
converters.AllowSharedBufferSource = (V, options = kEmptyObject) => {
  // AllowSharedBufferSource is a typedef for
  // (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView).
  // The union branches are disjoint, so this keeps the hot view path first.
  if (ArrayBufferIsView(V)) {
    const buffer = getViewedArrayBuffer(V);
    let resizable;
    try {
      resizable = ArrayBufferPrototypeGetResizable(buffer);
    } catch {
      validateAllowGrowableSharedArrayBuffer(buffer, options);
      return V;
    }
    validateAllowResizableArrayBuffer(resizable, options);
    return V;
  }

  let resizable;
  try {
    resizable = ArrayBufferPrototypeGetResizable(V);
  } catch {
    if (isSharedArrayBuffer(V)) {
      validateAllowGrowableSharedArrayBuffer(V, options);
      return V;
    }
    throw makeException(
      'is not instance of ArrayBuffer, SharedArrayBuffer, Buffer, ' +
      'TypedArray, or DataView.',
      options);
  }

  validateAllowResizableArrayBuffer(resizable, options);
  return V;
};

/**
 * Converts a JavaScript value to the IDL sequence<DOMString> type.
 * @see https://webidl.spec.whatwg.org/#es-sequence
 * @type {Converter}
 */
converters['sequence<DOMString>'] =
  createSequenceConverter(converters.DOMString);

/**
 * Converts a JavaScript value to the IDL sequence<object> type.
 * @see https://webidl.spec.whatwg.org/#es-sequence
 * @type {Converter}
 */
converters['sequence<object>'] =
  createSequenceConverter(converters.object);

// https://html.spec.whatwg.org/multipage/web-messaging.html#structuredserializeoptions
//
// dictionary StructuredSerializeOptions {
//   sequence<object> transfer = [];
// };
converters.StructuredSerializeOptions = createDictionaryConverter(
  'StructuredSerializeOptions',
  [
    {
      key: 'transfer',
      converter: converters['sequence<object>'],
      defaultValue: () => [],
    },
  ],
);

module.exports = {
  converters,
  convertToInt,
  createDictionaryConverter,
  createEnumConverter,
  createInterfaceConverter,
  createSequenceConverter,
  requiredArguments,
  type,
};