@ 261106.2
current
'use strict';

const {
  ArrayPrototypeFilter,
  ObjectKeys,
  PromisePrototypeThen,
  PromiseResolve,
  PromiseWithResolvers,
  SafePromiseAllReturnVoid,
  SafePromisePrototypeFinally,
  SafeSet,
  StringPrototypeStartsWith,
  Symbol,
  TypeError,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
  TypedArrayPrototypeGetByteOffset,
  Uint8Array,
} = primordials;

const { TextEncoder } = require('internal/encoding');

const {
  ReadableStream,
  isReadableStream,
  readableStreamDefaultReaderRead,
  kChunk,
  kClose,
  kError,
} = require('internal/webstreams/readablestream');

const {
  WritableStream,
  isWritableStream,
  writableStreamDefaultWriterIsStarted,
  writableStreamDefaultWriterWriteFailure,
  writableStreamDefaultWriterWriteWithRequest,
} = require('internal/webstreams/writablestream');

const {
  CountQueuingStrategy,
  ByteLengthQueuingStrategy,
} = require('internal/webstreams/queuingstrategies');

const {
  Writable,
  Readable,
  Duplex,
  destroy,
} = require('stream');

const {
  isDestroyed,
  isReadable,
  isWritable,
  isWritableEnded,
} = require('internal/streams/utils');

const {
  Buffer,
} = require('buffer');

const {
  kResolvedPromise,
} = require('internal/webstreams/util');

const {
  getContinuationPreservedEmbedderData: currentContextFrame,
  setContinuationPreservedEmbedderData: setContextFrame,
} = internalBinding('async_context_frame');

const {
  isAnyArrayBuffer,
} = require('internal/util/types');

const {
  AbortError,
  ErrnoException,
  codes: {
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_ARG_VALUE,
    ERR_INVALID_STATE,
    ERR_STREAM_PREMATURE_CLOSE,
  },
} = require('internal/errors');

const {
  getDeprecationWarningEmitter,
  kEmptyObject,
  normalizeEncoding,
  setOwnProperty,
} = require('internal/util');

const {
  validateBoolean,
  validateFunction,
  validateObject,
  validateOneOf,
} = require('internal/validators');

const {
  WriteWrap,
  ShutdownWrap,
  kArrayBufferOffset,
  kReadBytesOrError,
  kLastWriteWasAsync,
  streamBaseState,
} = internalBinding('stream_wrap');

const {
  eos,
  kEosNodeSynchronousCallback,
} = require('internal/streams/end-of-stream');

const { zlib } = internalBinding('constants');
const { UV_EOF } = internalBinding('uv');

const encoder = new TextEncoder();

const kValidateChunk = Symbol('kValidateChunk');
const kDestroyOnSyncError = Symbol('kDestroyOnSyncError');

// Collect all negative (error) ZLIB codes and Z_NEED_DICT
const ZLIB_FAILURES = new SafeSet(
  ArrayPrototypeFilter(
    ObjectKeys(zlib),
    (code) => code === 'Z_NEED_DICT' || zlib[code] < 0,
  ),
);

/**
 * @param {Error|null} cause
 * @returns {Error|null}
 */
function handleKnownInternalErrors(cause) {
  const causeCode = cause?.code;
  switch (true) {
    case causeCode === 'ERR_STREAM_PREMATURE_CLOSE': {
      return new AbortError(undefined, { cause });
    }
    case ZLIB_FAILURES.has(causeCode):
    // Brotli decoder error codes are formatted as 'ERR_' +
    // BrotliDecoderErrorString(), where the latter returns strings like
    // '_ERROR_FORMAT_...', '_ERROR_ALLOC_...', '_ERROR_UNREACHABLE', etc.
    // The resulting JS error codes all start with 'ERR__ERROR_'.
    // Falls through
    case causeCode != null &&
      StringPrototypeStartsWith(causeCode, 'ERR__ERROR_'): {
      // eslint-disable-next-line no-restricted-syntax
      const error = new TypeError(undefined, { cause });
      setOwnProperty(error, 'code', causeCode);
      return error;
    }
    default:
      return cause;
  }
}

const noop = () => {};

// A single shared write request tracks every chunk written to the
// destination WritableStream, replacing the per-write promise pair that
// writer.ready + writer.write() would allocate. Only one write/writev
// batch is in flight at a time, so one callback slot is enough.
// `promise` is non-undefined so writable-stream queue entries are
// discriminated from kNilRequest by `promise === undefined`;
// `failed`/`failure` latch a write that could not proceed. The callback
// runs in the async context of the Node.js write that set it, as the
// reaction to writer.write() did (see settle()).
function createWriteTracker(getDestination) {
  return {
    promise: null,
    pending: 0,
    failed: false,
    failure: undefined,
    callback: undefined,
    contextFrame: undefined,
    error: undefined,
    hasError: false,
    dispatching: false,
    resolve() {
      this.pending--;
      if (!this.dispatching && this.pending === 0 &&
          this.callback !== undefined) {
        this.settle();
      }
    },
    reject(error) {
      this.pending--;
      if (!this.hasError) {
        this.hasError = true;
        this.error = error;
      }
      if (!this.dispatching && this.pending === 0 &&
          this.callback !== undefined) {
        this.settle();
      }
    },
    settle() {
      const contextFrame = this.contextFrame;
      this.contextFrame = undefined;
      const prior = currentContextFrame();
      if (prior === contextFrame) {
        this.settleInContext();
        return;
      }
      // Completed from another async context (e.g. a write queued while
      // the destination was starting): deliver from a reaction registered
      // in the context of the write, once the destination has finished
      // processing it.
      setContextFrame(contextFrame);
      PromisePrototypeThen(kResolvedPromise, () => this.settleInContext());
      setContextFrame(prior);
    },
    settleInContext() {
      const callback = this.callback;
      this.callback = undefined;
      let error;
      if (this.hasError) {
        error = this.error;
        this.hasError = false;
        this.error = undefined;
      } else if (this.failed) {
        error = this.failure;
      }
      this.failed = false;
      this.failure = undefined;
      if (error !== undefined) {
        // Deliver failures in a microtask: this can run while the
        // writable-stream machinery is mid-transition (e.g. inside
        // writableStreamFinishErroring), and the callback typically
        // destroys the stream, which re-enters the machinery.
        PromisePrototypeThen(kResolvedPromise, () => {
          try {
            callback(error);
          } catch (err) {
            process.nextTick(() => destroy(getDestination(), err));
          }
        });
        return;
      }
      try {
        callback(error);
      } catch (err) {
        // In a next tick because this can run within a promise context,
        // and any error thrown must not become an unhandled rejection.
        process.nextTick(() => destroy(getDestination(), err));
      }
    },
  };
}

// A write issued before the destination has started would be queued and
// processed from the start step, in the async context of the code that
// created the WritableStream. Dispatch it from a writer.ready reaction
// instead, as the promise-based adapter did, so the sink runs in the
// context of the Node.js write. The stream may have been closed or
// errored (e.g. by destroy()) by then.
function deferDispatch(writer, tracker, dispatchNow) {
  const dispatch = () => {
    const failure = writableStreamDefaultWriterWriteFailure(writer);
    if (failure !== undefined) {
      tracker.failed = true;
      tracker.failure = failure;
      tracker.settle();
      return;
    }
    dispatchNow();
  };
  PromisePrototypeThen(writer.ready, dispatch, dispatch);
}

function dispatchWrite(writer, chunk, tracker) {
  tracker.contextFrame = currentContextFrame();
  if (!writableStreamDefaultWriterIsStarted(writer)) {
    deferDispatch(writer, tracker,
                  () => dispatchWriteNow(writer, chunk, tracker));
    return;
  }
  dispatchWriteNow(writer, chunk, tracker);
}

function dispatchWriteNow(writer, chunk, tracker) {
  tracker.dispatching = true;
  writableStreamDefaultWriterWriteWithRequest(writer, chunk, tracker);
  tracker.dispatching = false;
  if (tracker.pending === 0)
    tracker.settle();
}

function dispatchWritev(writer, chunks, tracker) {
  tracker.contextFrame = currentContextFrame();
  if (!writableStreamDefaultWriterIsStarted(writer)) {
    deferDispatch(writer, tracker,
                  () => dispatchWritevNow(writer, chunks, tracker));
    return;
  }
  dispatchWritevNow(writer, chunks, tracker);
}

function dispatchWritevNow(writer, chunks, tracker) {
  tracker.dispatching = true;
  for (let i = 0; i < chunks.length; i++)
    writableStreamDefaultWriterWriteWithRequest(writer, chunks[i].chunk, tracker);
  tracker.dispatching = false;
  if (tracker.pending === 0)
    tracker.settle();
}

/**
 * @typedef {import('../../stream').Writable} Writable
 * @typedef {import('../../stream').Readable} Readable
 * @typedef {import('./writablestream').WritableStream} WritableStream
 * @typedef {import('./readablestream').ReadableStream} ReadableStream
 */

/**
 * @typedef {import('../abort_controller').AbortSignal} AbortSignal
 */

/**
 * @param {Writable} streamWritable
 * @param {object} [options]
 * @returns {WritableStream}
 */
function newWritableStreamFromStreamWritable(streamWritable, options = kEmptyObject) {
  // Not using the internal/streams/utils isWritableNodeStream utility
  // here because it will return false if streamWritable is a Duplex
  // whose writable option is false. For a Duplex that is not writable,
  // we want it to pass this check but return a closed WritableStream.
  // We check if the given stream is a stream.Writable or http.OutgoingMessage
  const checkIfWritableOrOutgoingMessage =
    streamWritable &&
    typeof streamWritable?.write === 'function' &&
    typeof streamWritable?.on === 'function';
  if (!checkIfWritableOrOutgoingMessage) {
    throw new ERR_INVALID_ARG_TYPE(
      'streamWritable',
      'stream.Writable',
      streamWritable,
    );
  }

  if (isDestroyed(streamWritable) || !isWritable(streamWritable)) {
    const writable = new WritableStream();
    writable.close();
    return writable;
  }

  const highWaterMark = streamWritable.writableHighWaterMark;
  const strategy =
    streamWritable.writableObjectMode ?
      new CountQueuingStrategy({ highWaterMark }) :
      {
        highWaterMark,
        size(chunk) {
          return chunk?.byteLength ?? chunk?.length ?? 1;
        },
      };

  let controller;
  let backpressurePromise;
  let closed;

  function onDrain() {
    backpressurePromise?.resolve();
  }

  const cleanup = eos(streamWritable, (error) => {
    error = handleKnownInternalErrors(error);

    cleanup();
    // This is a protection against non-standard, legacy streams
    // that happen to emit an error event again after finished is called.
    streamWritable.on('error', () => {});
    if (error != null) {
      backpressurePromise?.reject(error);
      // If closed is not undefined, the error is happening
      // after the WritableStream close has already started.
      // We need to reject it here.
      if (closed !== undefined) {
        closed.reject(error);
        closed = undefined;
      }
      controller?.error(error);
      controller = undefined;
      return;
    }

    if (closed !== undefined) {
      closed.resolve();
      closed = undefined;
      return;
    }
    controller?.error(new AbortError());
    controller = undefined;
  });

  streamWritable.on('drain', onDrain);

  return new WritableStream({
    start(c) { controller = c; },

    write(chunk) {
      try {
        options[kValidateChunk]?.(chunk);
        if (!streamWritable.writableObjectMode && isAnyArrayBuffer(chunk)) {
          chunk = new Uint8Array(chunk);
        }
        if (streamWritable.writableNeedDrain || !streamWritable.write(chunk)) {
          backpressurePromise = PromiseWithResolvers();
          if (!streamWritable.writableNeedDrain) {
            backpressurePromise.resolve();
          }
          return SafePromisePrototypeFinally(
            backpressurePromise.promise, () => {
              backpressurePromise = undefined;
            });
        }
      } catch (error) {
        // When the kDestroyOnSyncError flag is set (e.g. for
        // CompressionStream), a sync throw must also destroy the
        // stream so the readable side is errored too. Without this
        // the readable side hangs forever. This replicates the
        // TransformStream semantics: error both sides on any throw
        // in the transform path.
        if (options[kDestroyOnSyncError]) {
          destroy(streamWritable, error);
        }
        throw error;
      }
    },

    abort(reason) {
      destroy(streamWritable, reason);
    },

    close() {
      if (closed === undefined && !isWritableEnded(streamWritable)) {
        closed = PromiseWithResolvers();
        streamWritable.end();
        return closed.promise;
      }

      controller = undefined;
      return PromiseResolve();
    },
  }, strategy);
}

/**
 * @param {WritableStream} writableStream
 * @param {{
 *   decodeStrings? : boolean,
 *   highWaterMark? : number,
 *   objectMode? : boolean,
 *   signal? : AbortSignal,
 * }} [options]
 * @returns {Writable}
 */
function newStreamWritableFromWritableStream(writableStream, options = kEmptyObject) {
  if (!isWritableStream(writableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      'writableStream',
      'WritableStream',
      writableStream);
  }

  validateObject(options, 'options');
  const {
    highWaterMark,
    decodeStrings = true,
    objectMode = false,
    signal,
  } = options;

  validateBoolean(objectMode, 'options.objectMode');
  validateBoolean(decodeStrings, 'options.decodeStrings');

  const writer = writableStream.getWriter();
  let closed = false;
  const writeTracker = createWriteTracker(() => writable);

  const writable = new Writable({
    highWaterMark,
    objectMode,
    decodeStrings,
    signal,

    writev(chunks, callback) {
      writeTracker.callback = callback;
      dispatchWritev(writer, chunks, writeTracker);
    },

    write(chunk, encoding, callback) {
      if (typeof chunk === 'string' && decodeStrings && !objectMode) {
        const enc = normalizeEncoding(encoding);

        if (enc === 'utf8') {
          chunk = encoder.encode(chunk);
        } else {
          chunk = Buffer.from(chunk, encoding);
          chunk = new Uint8Array(
            TypedArrayPrototypeGetBuffer(chunk),
            TypedArrayPrototypeGetByteOffset(chunk),
            TypedArrayPrototypeGetByteLength(chunk),
          );
        }
      }

      writeTracker.callback = callback;
      dispatchWrite(writer, chunk, writeTracker);
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          process.nextTick(() => { throw error; });
        }
      }

      if (!closed) {
        if (error != null) {
          PromisePrototypeThen(
            writer.abort(error),
            done,
            done);
        } else {
          PromisePrototypeThen(
            writer.close(),
            done,
            done);
        }
        return;
      }

      done();
    },

    final(callback) {
      function done(error) {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          process.nextTick(() => destroy(writable, error));
        }
      }

      if (!closed) {
        PromisePrototypeThen(
          writer.close(),
          done,
          done);
      }
    },
  });

  PromisePrototypeThen(
    writer.closed,
    () => {
      // If the WritableStream closes before the stream.Writable has been
      // ended, we signal an error on the stream.Writable.
      closed = true;
      if (!isWritableEnded(writable))
        destroy(writable, new ERR_STREAM_PREMATURE_CLOSE());
    },
    (error) => {
      // If the WritableStream errors before the stream.Writable has been
      // destroyed, signal an error on the stream.Writable.
      closed = true;
      destroy(writable, error);
    });

  return writable;
}

const kErrorSentinelAttached = Symbol('kErrorSentinelAttached');

/**
 * @typedef {import('./queuingstrategies').QueuingStrategy} QueuingStrategy
 * @param {Readable} streamReadable
 * @param {{
 *  strategy? : QueuingStrategy
 *  type? : 'bytes',
 * }} [options]
 * @returns {ReadableStream}
 */
function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObject) {
  // Not using the internal/streams/utils isReadableNodeStream utility
  // here because it will return false if streamReadable is a Duplex
  // whose readable option is false. For a Duplex that is not readable,
  // we want it to pass this check but return a closed ReadableStream.
  if (typeof streamReadable?._readableState !== 'object') {
    throw new ERR_INVALID_ARG_TYPE(
      'streamReadable',
      'stream.Readable',
      streamReadable);
  }
  validateObject(options, 'options');
  if (options.type !== undefined) {
    validateOneOf(options.type, 'options.type', ['bytes', undefined]);
  }

  const isBYOB = options.type === 'bytes';
  let controller;
  let wasCanceled = false;
  let strategy;

  /** @type {UnderlyingSource} */
  const underlyingSource = {
    __proto__: null,
    type: isBYOB ? 'bytes' : undefined,
    start(c) { controller = c; },
    cancel(reason) {
      wasCanceled = true;
      destroy(streamReadable, reason);
    },
  };

  const readable = isReadable(streamReadable);
  const objectMode = streamReadable.readableObjectMode;
  if (readable) {
    underlyingSource.pull = function pull() {
      streamReadable.resume();
    };

    const highWaterMark = streamReadable.readableHighWaterMark;
    strategy = isBYOB ? { highWaterMark } :
      options.strategy ?? new (objectMode ? CountQueuingStrategy : ByteLengthQueuingStrategy)({ highWaterMark });
  }
  const readableStream = new ReadableStream(underlyingSource, strategy);

  // When adapting a Duplex as a ReadableStream, readable completion should not
  // wait for a half-open writable side to finish as well.
  let cleanup = noop;
  cleanup = eos(streamReadable, {
    __proto__: null,
    writable: false,
    [kEosNodeSynchronousCallback]: true,
  }, (error) => {
    error = handleKnownInternalErrors(error);

    // If eos calls the callback synchronously, cleanup is still a no-op here.
    cleanup();

    if (!(kErrorSentinelAttached in streamReadable)) {
      // This is a protection against non-standard, legacy streams
      // that happen to emit an error event again after finished is called.
      streamReadable.on('error', noop);
      streamReadable[kErrorSentinelAttached] = true;
    }
    if (wasCanceled) {
      return;
    }
    wasCanceled = true;
    if (error)
      return controller.error(error);
    controller.close();
    if (isBYOB)
      controller.byobRequest?.respond(0);
  });

  if (wasCanceled) {
    // `eos` called the callback synchronously
    cleanup();
  } else if (readable) {
    streamReadable.pause();

    streamReadable.on('data', function onData(chunk) {
      if (wasCanceled) return;
      // Copy the Buffer to detach it from the pool.
      if (Buffer.isBuffer(chunk) && !objectMode)
        chunk = new Uint8Array(chunk);
      controller.enqueue(chunk);
      if (controller.desiredSize <= 0)
        streamReadable.pause();
    });
  }

  return readableStream;
}

/**
 * @param {ReadableStream} readableStream
 * @param {{
 *   highWaterMark? : number,
 *   encoding? : string,
 *   objectMode? : boolean,
 *   signal? : AbortSignal,
 * }} [options]
 * @returns {Readable}
 */
function newStreamReadableFromReadableStream(readableStream, options = kEmptyObject) {
  if (!isReadableStream(readableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      'readableStream',
      'ReadableStream',
      readableStream);
  }

  validateObject(options, 'options');
  const {
    highWaterMark,
    encoding,
    objectMode = false,
    signal,
  } = options;

  if (encoding !== undefined && !Buffer.isEncoding(encoding))
    throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
  validateBoolean(objectMode, 'options.objectMode');

  const reader = readableStream.getReader();
  let closed = false;
  let readRequest;
  let pendingChunk;

  function forwardChunk() {
    const chunk = pendingChunk;
    pendingChunk = undefined;
    readable.push(chunk);
  }

  const readable = new Readable({
    objectMode,
    highWaterMark,
    encoding,
    signal,

    read() {
      // At most one read is in flight (Readable does not call read()
      // again before push()), so a single reused read request replaces
      // the promise, read-result object, and reaction closures that
      // reader.read() would allocate per chunk. The chunk is forwarded
      // in a microtask to keep the old reaction position: a sync push
      // could deliver a chunk that an error or destroy should beat.
      readRequest ??= {
        [kChunk](chunk) {
          pendingChunk = chunk;
          PromisePrototypeThen(kResolvedPromise, forwardChunk);
        },
        [kClose]() {
          readable.push(null);
        },
        [kError](error) {
          destroy(readable, error);
        },
      };
      readableStreamDefaultReaderRead(reader, readRequest);
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          process.nextTick(() => { throw error; });
        }
      }

      if (!closed) {
        PromisePrototypeThen(
          reader.cancel(error),
          done,
          done);
        return;
      }
      done();
    },
  });

  PromisePrototypeThen(
    reader.closed,
    () => {
      closed = true;
    },
    (error) => {
      closed = true;
      destroy(readable, error);
    });

  return readable;
}

const emitDEP0201 = getDeprecationWarningEmitter(
  'DEP0201',
  "Passing 'options.type' to Duplex.toWeb() is deprecated. " +
      "To specify the ReadableStream type, use 'options.readableType'.",
  newReadableWritablePairFromDuplex);

/**
 * @typedef {import('./readablestream').ReadableWritablePair
 * } ReadableWritablePair
 * @typedef {import('../../stream').Duplex} Duplex
 */

/**
 * @param {Duplex} duplex
 * @param {{ readableType?: 'bytes' }} [options]
 * @returns {ReadableWritablePair}
 */
function newReadableWritablePairFromDuplex(duplex, options = kEmptyObject) {
  // Not using the internal/streams/utils isWritableNodeStream and
  // isReadableNodeStream utilities here because they will return false
  // if the duplex was created with writable or readable options set to
  // false. Instead, we'll check the readable and writable state after
  // and return closed WritableStream or closed ReadableStream as
  // necessary.
  if (typeof duplex?._writableState !== 'object' ||
      typeof duplex?._readableState !== 'object') {
    throw new ERR_INVALID_ARG_TYPE('duplex', 'stream.Duplex', duplex);
  }

  validateObject(options, 'options');

  const readableOptions = {
    __proto__: null,
    type: options.readableType,
  };

  if (options.readableType == null && options.type != null) {
    // 'options.type' is a deprecated alias for 'options.readableType'
    emitDEP0201();
    readableOptions.type = options.type;
  }

  if (isDestroyed(duplex)) {
    const writable = new WritableStream();
    const readable = new ReadableStream({ type: readableOptions.type });
    writable.close();
    readable.cancel();
    return { readable, writable };
  }

  const writableOptions = {
    __proto__: null,
    [kValidateChunk]: options[kValidateChunk],
    [kDestroyOnSyncError]: options[kDestroyOnSyncError],
  };

  const writable =
    isWritable(duplex) ?
      newWritableStreamFromStreamWritable(duplex, writableOptions) :
      new WritableStream();

  if (!isWritable(duplex))
    writable.close();

  const readable =
    isReadable(duplex) ?
      newReadableStreamFromStreamReadable(duplex, readableOptions) :
      new ReadableStream({ type: readableOptions.type });

  if (!isReadable(duplex))
    readable.cancel();

  return { writable, readable };
}

/**
 * @param {ReadableWritablePair} pair
 * @param {{
 *   allowHalfOpen? : boolean,
 *   decodeStrings? : boolean,
 *   encoding? : string,
 *   highWaterMark? : number,
 *   objectMode? : boolean,
 *   signal? : AbortSignal,
 * }} [options]
 * @returns {Duplex}
 */
function newStreamDuplexFromReadableWritablePair(pair = kEmptyObject, options = kEmptyObject) {
  validateObject(pair, 'pair');
  const {
    readable: readableStream,
    writable: writableStream,
  } = pair;

  if (!isReadableStream(readableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      'pair.readable',
      'ReadableStream',
      readableStream);
  }
  if (!isWritableStream(writableStream)) {
    throw new ERR_INVALID_ARG_TYPE(
      'pair.writable',
      'WritableStream',
      writableStream);
  }

  validateObject(options, 'options');
  const {
    allowHalfOpen = false,
    objectMode = false,
    encoding,
    decodeStrings = true,
    highWaterMark,
    signal,
  } = options;

  validateBoolean(objectMode, 'options.objectMode');
  if (encoding !== undefined && !Buffer.isEncoding(encoding))
    throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);

  const writer = writableStream.getWriter();
  const reader = readableStream.getReader();
  let writableClosed = false;
  let readableClosed = false;
  let readRequest;
  let pendingChunk;
  const writeTracker = createWriteTracker(() => duplex);

  function forwardChunk() {
    const chunk = pendingChunk;
    pendingChunk = undefined;
    duplex.push(chunk);
  }

  const duplex = new Duplex({
    allowHalfOpen,
    highWaterMark,
    objectMode,
    encoding,
    decodeStrings,
    signal,

    writev(chunks, callback) {
      writeTracker.callback = callback;
      dispatchWritev(writer, chunks, writeTracker);
    },

    write(chunk, encoding, callback) {
      if (typeof chunk === 'string' && decodeStrings && !objectMode) {
        const enc = normalizeEncoding(encoding);

        if (enc === 'utf8') {
          chunk = encoder.encode(chunk);
        } else {
          chunk = Buffer.from(chunk, encoding);
          chunk = new Uint8Array(
            TypedArrayPrototypeGetBuffer(chunk),
            TypedArrayPrototypeGetByteOffset(chunk),
            TypedArrayPrototypeGetByteLength(chunk),
          );
        }
      }

      writeTracker.callback = callback;
      dispatchWrite(writer, chunk, writeTracker);
    },

    final(callback) {
      function done(error) {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          process.nextTick(() => destroy(duplex, error));
        }
      }

      if (!writableClosed) {
        PromisePrototypeThen(
          writer.close(),
          done,
          done);
      }
    },

    read() {
      // Same single-in-flight reused read request as
      // newStreamReadableFromReadableStream().
      readRequest ??= {
        [kChunk](chunk) {
          pendingChunk = chunk;
          PromisePrototypeThen(kResolvedPromise, forwardChunk);
        },
        [kClose]() {
          duplex.push(null);
        },
        [kError](error) {
          destroy(duplex, error);
        },
      };
      readableStreamDefaultReaderRead(reader, readRequest);
    },

    destroy(error, callback) {
      function done() {
        try {
          callback(error);
        } catch (error) {
          // In a next tick because this is happening within
          // a promise context, and if there are any errors
          // thrown we don't want those to cause an unhandled
          // rejection. Let's just escape the promise and
          // handle it separately.
          process.nextTick(() => { throw error; });
        }
      }

      async function closeWriter() {
        if (!writableClosed)
          await writer.abort(error);
      }

      async function closeReader() {
        if (!readableClosed)
          await reader.cancel(error);
      }

      if (!writableClosed || !readableClosed) {
        PromisePrototypeThen(
          SafePromiseAllReturnVoid([
            closeWriter(),
            closeReader(),
          ]),
          done,
          done);
        return;
      }

      done();
    },
  });

  PromisePrototypeThen(
    writer.closed,
    () => {
      writableClosed = true;
      if (!isWritableEnded(duplex))
        destroy(duplex, new ERR_STREAM_PREMATURE_CLOSE());
    },
    (error) => {
      writableClosed = true;
      readableClosed = true;
      destroy(duplex, error);
    });

  PromisePrototypeThen(
    reader.closed,
    () => {
      readableClosed = true;
    },
    (error) => {
      writableClosed = true;
      readableClosed = true;
      destroy(duplex, error);
    });

  return duplex;
}

/**
 * @typedef {import('./queuingstrategies').QueuingStrategy} QueuingStrategy
 * @param {object} streamBase
 * @param {QueuingStrategy} strategy
 * @returns {WritableStream}
 */
function newWritableStreamFromStreamBase(streamBase, strategy) {
  validateObject(streamBase, 'streamBase');

  let current;

  function createWriteWrap(controller, promise) {
    const req = new WriteWrap();
    req.handle = streamBase;
    req.oncomplete = onWriteComplete;
    req.async = false;
    req.bytes = 0;
    req.buffer = null;
    req.controller = controller;
    req.promise = promise;
    return req;
  }

  function onWriteComplete(status) {
    if (status < 0) {
      const error = new ErrnoException(status, 'write', this.error);
      this.promise.reject(error);
      this.controller.error(error);
      return;
    }
    this.promise.resolve();
  }

  function doWrite(chunk, controller) {
    const promise = PromiseWithResolvers();
    let ret;
    let req;
    try {
      req = createWriteWrap(controller, promise);
      ret = streamBase.writeBuffer(req, chunk);
      if (streamBaseState[kLastWriteWasAsync])
        req.buffer = chunk;
      req.async = !!streamBaseState[kLastWriteWasAsync];
    } catch (error) {
      promise.reject(error);
    }

    if (ret !== 0)
      promise.reject(new ErrnoException(ret, 'write', req));
    else if (!req.async)
      promise.resolve();

    return promise.promise;
  }

  return new WritableStream({
    write(chunk, controller) {
      current = current !== undefined ?
        PromisePrototypeThen(
          current,
          () => doWrite(chunk, controller),
          (error) => controller.error(error)) :
        doWrite(chunk, controller);
      return current;
    },

    close() {
      const promise = PromiseWithResolvers();
      const req = new ShutdownWrap();
      req.oncomplete = () => promise.resolve();
      const err = streamBase.shutdown(req);
      if (err === 1)
        promise.resolve();
      return promise.promise;
    },
  }, strategy);
}

/**
 * @param {StreamBase} streamBase
 * @param {QueuingStrategy} strategy
 * @returns {ReadableStream}
 */
function newReadableStreamFromStreamBase(streamBase, strategy, options = kEmptyObject) {
  validateObject(streamBase, 'streamBase');
  validateObject(options, 'options');

  const {
    ondone = () => {},
  } = options;

  if (typeof streamBase.onread === 'function')
    throw new ERR_INVALID_STATE('StreamBase already has a consumer');

  validateFunction(ondone, 'options.ondone');

  let controller;

  streamBase.onread = (arrayBuffer) => {
    const nread = streamBaseState[kReadBytesOrError];

    if (nread === 0)
      return;

    try {
      if (nread === UV_EOF) {
        controller.close();
        streamBase.readStop();
        try {
          ondone();
        } catch (error) {
          controller.error(error);
        }
        return;
      }

      const offset = streamBaseState[kArrayBufferOffset];
      controller.enqueue(new Uint8Array(arrayBuffer, offset, nread));

      if (controller.desiredSize <= 0)
        streamBase.readStop();
    } catch (error) {
      controller.error(error);
      streamBase.readStop();
    }
  };

  return new ReadableStream({
    start(c) { controller = c; },

    pull() {
      streamBase.readStart();
    },

    cancel() {
      const promise = PromiseWithResolvers();
      try {
        ondone();
      } catch (error) {
        promise.reject(error);
        return promise.promise;
      }
      const req = new ShutdownWrap();
      req.oncomplete = () => promise.resolve();
      const err = streamBase.shutdown(req);
      if (err === 1)
        promise.resolve();
      return promise.promise;
    },
  }, strategy);
}

module.exports = {
  newWritableStreamFromStreamWritable,
  newReadableStreamFromStreamReadable,
  newStreamWritableFromWritableStream,
  newStreamReadableFromReadableStream,
  newReadableWritablePairFromDuplex,
  newStreamDuplexFromReadableWritablePair,
  newWritableStreamFromStreamBase,
  newReadableStreamFromStreamBase,
  kValidateChunk,
  kDestroyOnSyncError,
};