current commit
260925.54 · lib: fix stream loading bug in node:bench
'use strict';

const {
  ArrayFrom,
  ArrayIsArray,
  ArrayPrototypePush,
  MapPrototypeClear,
  MapPrototypeEntries,
  MapPrototypeSet,
  ObjectDefineProperty,
  ObjectGetOwnPropertyDescriptor,
  ObjectGetOwnPropertyNames,
  ObjectGetPrototypeOf,
  ObjectKeys,
  ObjectPrototype,
  ObjectPrototypeHasOwnProperty,
  ObjectSetPrototypeOf,
  PromiseReject,
  PromiseResolve,
  PromiseWithResolvers,
  SafeMap,
  SetPrototypeAdd,
  SetPrototypeClear,
  SetPrototypeValues,
  Symbol,
} = primordials;
const { Readable } = require('stream');
const { deserializeError, serializeError } = require('internal/error_serdes');
const {
  codes: {
    ERR_INVALID_STATE,
  },
} = require('internal/errors');
const { isError } = require('internal/util');
const { isMap, isSet } = require('internal/util/types');
const { structuredClone } = require('internal/worker/js_transferable');

const kEmitMessage = Symbol('kEmitMessage');

function repairError(source, clone, seen) {
  const serialized = deserializeError(serializeError(source));
  let sourceName;
  try {
    sourceName = source.name;
  } catch {
    // The serialized form already omits properties whose getters throw.
  }
  let repaired = clone;
  if (!isError(repaired) ||
      (sourceName !== undefined && repaired.name !== sourceName)) {
    repaired = serialized;
  }
  if (repaired === null || typeof repaired !== 'object') return repaired;
  seen.set(source, repaired);

  if (serialized !== null && typeof serialized === 'object') {
    const serializedKeys = ObjectGetOwnPropertyNames(serialized);
    for (let i = 0; i < serializedKeys.length; i++) {
      const key = serializedKeys[i];
      if (ObjectPrototypeHasOwnProperty(repaired, key)) continue;
      const descriptor = ObjectGetOwnPropertyDescriptor(serialized, key);
      ObjectSetPrototypeOf(descriptor, null);
      ObjectDefineProperty(repaired, key, descriptor);
    }
  }

  const keys = ObjectGetOwnPropertyNames(source);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const descriptor = ObjectGetOwnPropertyDescriptor(source, key);
    if (!ObjectPrototypeHasOwnProperty(descriptor, 'value') ||
        typeof descriptor.value === 'function' ||
        typeof descriptor.value === 'symbol') {
      continue;
    }
    const existing = ObjectGetOwnPropertyDescriptor(repaired, key);
    const value = descriptor.value !== null &&
      typeof descriptor.value === 'object' ?
      repairClone(descriptor.value, existing?.value, seen) : descriptor.value;
    if ((existing !== undefined && existing.value === value) ||
        existing?.configurable === false) {
      continue;
    }
    ObjectDefineProperty(repaired, key, {
      __proto__: null,
      configurable: descriptor.configurable,
      enumerable: descriptor.enumerable,
      value,
      writable: descriptor.writable,
    });
  }
  return repaired;
}

function repairClone(source, clone, seen) {
  if (source === null || (typeof source !== 'object' &&
                          typeof source !== 'function')) {
    return clone === undefined ? source : clone;
  }
  if (typeof source === 'function') return clone;
  if (seen.has(source)) return seen.get(source);
  if (isError(source)) return repairError(source, clone, seen);
  if (clone === null || typeof clone !== 'object') {
    try {
      clone = structuredClone(source);
    } catch {
      const prototype = ObjectGetPrototypeOf(source);
      if (!ArrayIsArray(source) && prototype !== null &&
          prototype !== ObjectPrototype) {
        return clone;
      }
      clone = ArrayIsArray(source) ? [] : { __proto__: prototype };
    }
  }
  seen.set(source, clone);
  if (isMap(source) && isMap(clone)) {
    const sourceEntries = ArrayFrom(MapPrototypeEntries(source));
    const cloneEntries = ArrayFrom(MapPrototypeEntries(clone));
    const repairedEntries = [];
    for (let i = 0; i < sourceEntries.length; i++) {
      ArrayPrototypePush(repairedEntries, [
        repairClone(sourceEntries[i][0], cloneEntries[i][0], seen),
        repairClone(sourceEntries[i][1], cloneEntries[i][1], seen),
      ]);
    }
    MapPrototypeClear(clone);
    for (let i = 0; i < repairedEntries.length; i++) {
      MapPrototypeSet(clone, repairedEntries[i][0], repairedEntries[i][1]);
    }
    return clone;
  }
  if (isSet(source) && isSet(clone)) {
    const sourceValues = ArrayFrom(SetPrototypeValues(source));
    const cloneValues = ArrayFrom(SetPrototypeValues(clone));
    const repairedValues = [];
    for (let i = 0; i < sourceValues.length; i++) {
      ArrayPrototypePush(
        repairedValues, repairClone(sourceValues[i], cloneValues[i], seen));
    }
    SetPrototypeClear(clone);
    for (let i = 0; i < repairedValues.length; i++) {
      SetPrototypeAdd(clone, repairedValues[i]);
    }
    return clone;
  }
  const prototype = ObjectGetPrototypeOf(source);
  if (prototype === null) ObjectSetPrototypeOf(clone, null);
  if (prototype !== null && prototype !== ObjectPrototype &&
      !ArrayIsArray(source)) {
    return clone;
  }
  const keys = ObjectKeys(source);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const descriptor = ObjectGetOwnPropertyDescriptor(source, key);
    if (!ObjectPrototypeHasOwnProperty(descriptor, 'value')) continue;
    const existing = ObjectGetOwnPropertyDescriptor(clone, key);
    const value = repairClone(descriptor.value, existing?.value, seen);
    if ((existing !== undefined && existing.value === value) ||
        existing?.configurable === false) {
      continue;
    }
    ObjectDefineProperty(clone, key, {
      __proto__: null,
      configurable: descriptor.configurable,
      enumerable: descriptor.enumerable,
      value,
      writable: descriptor.writable,
    });
  }
  if (ArrayIsArray(source)) {
    const descriptor = ObjectGetOwnPropertyDescriptor(source, 'length');
    ObjectSetPrototypeOf(descriptor, null);
    ObjectDefineProperty(clone, 'length', descriptor);
  }
  return clone;
}

function cloneRecordData(data) {
  let clone;
  try {
    clone = structuredClone(data);
  } catch (error) {
    if (data.error === undefined) throw error;
    clone = structuredClone({ __proto__: null, ...data, error: undefined });
    clone.error = deserializeError(serializeError(data.error));
  }
  try {
    return repairClone(data, clone, new SafeMap());
  } catch {
    return clone;
  }
}

class BenchmarksStream extends Readable {
  #blocked = false;
  #drainWaiters = [];
  #hasReader = false;

  constructor() {
    super({
      __proto__: null,
      objectMode: true,
    });
  }

  _read() {
    if (this.#blocked) {
      this.#blocked = false;
      const waiters = this.#drainWaiters;
      this.#drainWaiters = [];
      for (let i = 0; i < waiters.length; i++) waiters[i].resolve();
    }
  }

  read(size) {
    if (size !== 0) this.#hasReader = true;
    return super.read(size);
  }

  _destroy(error, callback) {
    const failure = error ??
      new ERR_INVALID_STATE('benchmark stream is closed');
    const waiters = this.#drainWaiters;
    this.#drainWaiters = [];
    for (let i = 0; i < waiters.length; i++) waiters[i].reject(failure);
    callback(error);
  }

  waitForDrain() {
    if (this.destroyed) {
      return PromiseReject(this.errored ??
        new ERR_INVALID_STATE('benchmark stream is closed'));
    }
    if (!this.#blocked) return PromiseResolve();
    const waiter = PromiseWithResolvers();
    ArrayPrototypePush(this.#drainWaiters, waiter);
    return waiter.promise;
  }

  plan(data) {
    return this[kEmitMessage]('bench:plan', data);
  }

  start(data) {
    return this[kEmitMessage]('bench:start', data);
  }

  sample(data) {
    return this[kEmitMessage]('bench:sample', data);
  }

  complete(data) {
    return this[kEmitMessage]('bench:complete', data);
  }

  diagnostic(data) {
    return this[kEmitMessage]('bench:diagnostic', data);
  }

  summary(data) {
    return this[kEmitMessage]('bench:summary', data);
  }

  end() {
    return this.#tryPush(null);
  }

  [kEmitMessage](type, data) {
    const recordData = cloneRecordData(data);
    const record = { __proto__: null, type, data: recordData };
    if (this.listenerCount(type) > 0) {
      this.emit(type, cloneRecordData(recordData));
    }
    return this.#tryPush(record);
  }

  #tryPush(record) {
    if (this.destroyed) return false;
    const canPush = this.push(record);
    if (record !== null && !canPush && this.#hasReader) {
      this.#blocked = true;
      return false;
    }
    return true;
  }
}

module.exports = {
  BenchmarksStream,
};