current commit
260920.22 · lib: add primitive fast path to structuredClone
'use strict';

require('../common');
const assert = require('assert');

const prefix = "Failed to execute 'structuredClone'";
const key = 'transfer';
const context = 'Options';
const memberConverterError = `${prefix}: ${key} in ${context} cannot 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', 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);
assert.strictEqual(structuredClone(undefined, null), undefined);
// Transfer can be null or undefined.
assert.strictEqual(structuredClone(undefined, { }), undefined);

// Primitives clone to themselves.
assert.strictEqual(structuredClone(null), null);
assert.strictEqual(structuredClone(1), 1);
assert.strictEqual(structuredClone('x'), 'x');
assert.strictEqual(structuredClone(true), true);
assert.strictEqual(structuredClone(10n), 10n);
// -0 is preserved.
assert.ok(Object.is(structuredClone(-0), -0));
// Symbols are not cloneable and must throw.
assert.throws(() => structuredClone(Symbol()), { name: 'DataCloneError' });

// Transferables or its subclasses should be received with its closest transferable superclass
for (const StreamClass of [ReadableStream, WritableStream, TransformStream]) {
  const original = new StreamClass();
  const transfer = structuredClone(original, { transfer: [original] });
  assert.strictEqual(Object.getPrototypeOf(transfer), StreamClass.prototype);
  assert.ok(transfer instanceof StreamClass);

  const extended = class extends StreamClass {};
  const extendedOriginal = new extended();
  const extendedTransfer = structuredClone(extendedOriginal, { transfer: [extendedOriginal] });
  assert.strictEqual(Object.getPrototypeOf(extendedTransfer), StreamClass.prototype);
  assert.ok(extendedTransfer instanceof StreamClass);
}

for (const Transferrable of [File, Blob]) {
  const a2 = Transferrable === File ? '' : {};
  const original = new Transferrable([], a2);
  const transfer = structuredClone(original);
  assert.strictEqual(Object.getPrototypeOf(transfer), Transferrable.prototype);
  assert.ok(transfer instanceof Transferrable);

  const extendedOriginal = new (class extends Transferrable {})([], a2);
  const extendedTransfer = structuredClone(extendedOriginal);
  assert.strictEqual(Object.getPrototypeOf(extendedTransfer), Transferrable.prototype);
  assert.ok(extendedTransfer instanceof Transferrable);
}

// Transfer can be iterable
{
  const value = {
    a: new ReadableStream(),
    b: new WritableStream(),
  };
  const cloned = structuredClone(value, {
    transfer: {
      *[Symbol.iterator]() {
        for (const key in value) {
          yield value[key];
        }
      }
    }
  });
  for (const key in value) {
    assert.ok(value[key].locked);
    assert.ok(!cloned[key].locked);
  }
}

{
  // See: https://github.com/nodejs/node/issues/49940
  const cloned = structuredClone({}, {
    transfer: {
      *[Symbol.iterator]() {}
    }
  });

  assert.deepStrictEqual(cloned, {});
}

const blob = new Blob();
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });