Commit 9e6dd49a127 for nodejs

commit 9e6dd49a1272bde85ba1156b4639a0a762e6836a
Author: Ethan Arrowood <ethan@arrowood.dev>
Date:   Fri Sep 4 10:21:32 2026 -0600

    stream: add dump()/dumpSync() for stream/iter

    Every consumer in node:stream/iter retains what it reads, so there is
    no way to read a streamable to completion while keeping nothing. To
    clear a stream, callers write `for await (const _ of source) {}`. This
    is apparent throughout multiple test suites, including the stream/iter
    tests themselves.

    These are especially useful for QUIC where a receiver doesn't want the
    payload, but still has to read it to completion to relieve
    backpressure. bytes() does that too, but allocates the whole payload to
    discard it.

    dump() pulls every batch and drops it, so peak memory is one batch
    regardless of volume. It takes the same signal and limit options as
    the other consumers, rejects if the source errors mid-stream, and
    fulfills with undefined. dumpSync() is the synchronous form.

    The name follows undici's body.dump(). drain() was the obvious
    alternative but this module already exports ondrain() and
    drainableProtocol() for writer-side backpressure, which is the
    opposite side of the pipe.

    Assisted-by: Claude Opus 5
    Signed-off-by: Ethan Arrowood <ethan@arrowood.dev>
    PR-URL: https://github.com/nodejs/node/pull/65598
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
    Reviewed-By: Tim Perry <pimterry@gmail.com>

diff --git a/doc/api/quic.md b/doc/api/quic.md
index 05de774dbd9..36e4920860b 100644
--- a/doc/api/quic.md
+++ b/doc/api/quic.md
@@ -350,6 +350,10 @@ Only one async iterator can be obtained per stream. The stream is also
 compatible with `node:stream/iter` utilities such as `Stream.bytes()`,
 `Stream.text()`, and `Stream.pipeTo()`.

+Consuming a stream is what returns flow-control credit to the peer, so a
+stream whose payload is not wanted should still be read to completion. Use
+`Stream.dump()` to read the stream without retaining any of it.
+
 ### Datagrams

 In addition to streams, QUIC supports unreliable datagrams ([RFC 9221][]) for
diff --git a/doc/api/stream_iter.md b/doc/api/stream_iter.md
index 44a6d848a21..e8cc8c63632 100644
--- a/doc/api/stream_iter.md
+++ b/doc/api/stream_iter.md
@@ -1065,6 +1065,81 @@ added:

 Synchronous version of [`bytes()`][].

+### `dump(source[, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `source` {AsyncIterable|Iterable} whose chunks must be {Uint8Array\[]}
+* `options` {Object}
+  * `signal` {AbortSignal}
+  * `limit` {number} Maximum number of bytes to consume. If the total bytes
+    read exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown
+* Returns: {Promise} Fulfills with `undefined`.
+
+Read a source to completion, discarding every chunk. Unlike the other
+consumers, `dump()` retains nothing. Memory tops-out at a single batch no
+matter how much data the source produces.
+
+Use this to consume a stream when the content doesn't matter. For example, A
+QUIC stream only returns flow-control credit to the peer as its data is
+consumed, so a receiver that does not want the payload must still read it to
+completion.
+
+If the source errors part-way through, the returned promise rejects with that
+error.
+
+There is no default `limit`. `dump()` reads until the source is exhausted
+unless a limit is specified. When a limit is configured and the source exceeds
+it, the promise rejects and the source is cancelled. A partial dump is never
+reported as success.
+
+```mjs
+import { dump, from, pull, tap } from 'node:stream/iter';
+
+// Count the bytes flowing through a stream without retaining any of them.
+let bytesSeen = 0;
+const counter = tap((chunks) => {
+  for (const chunk of chunks) bytesSeen += chunk.byteLength;
+});
+
+await dump(pull(from('hello world'), counter));
+console.log(bytesSeen); // 11
+```
+
+```cjs
+const { dump, from, pull, tap } = require('node:stream/iter');
+
+async function run() {
+  // Count the bytes flowing through a stream without retaining any of them.
+  let bytesSeen = 0;
+  const counter = tap((chunks) => {
+    for (const chunk of chunks) bytesSeen += chunk.byteLength;
+  });
+
+  await dump(pull(from('hello world'), counter));
+  console.log(bytesSeen); // 11
+}
+
+run().catch(console.error);
+```
+
+### `dumpSync(source[, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `source` {Iterable} whose chunks must be {Uint8Array\[]}
+* `options` {Object}
+  * `limit` {number} Maximum number of bytes to consume. If the total bytes
+    read exceeds limit, an `ERR_OUT_OF_RANGE` error is thrown
+* Returns: {undefined}
+
+Synchronous version of [`dump()`][]. Throws `ERR_INVALID_ARG_TYPE` if `source`
+is not synchronously iterable.
+
 ### `text(source[, options])`

 <!-- YAML
@@ -2247,6 +2322,7 @@ console.log(textSync(stream)); // 'hello world'
 [`array()`]: #arraysource-options
 [`arrayBuffer()`]: #arraybuffersource-options
 [`bytes()`]: #bytessource-options
+[`dump()`]: #drainsource-options
 [`from()`]: #frominput
 [`fromSync()`]: #fromsyncinput
 [`node:zlib/iter`]: zlib.md#iterable-compression
diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js
index 9e9a8fde4ec..d2ecf5f3e21 100644
--- a/lib/internal/streams/iter/consumers.js
+++ b/lib/internal/streams/iter/consumers.js
@@ -3,6 +3,7 @@
 // New Streams API - Consumers & Utilities
 //
 // bytes(), text(), arrayBuffer() - collect entire stream
+// dump() - consume entire stream, retaining nothing
 // tap(), tapSync() - observe without modifying
 // merge() - temporal combining of sources
 // ondrain() - backpressure drain utility
@@ -271,6 +272,35 @@ function arraySync(source, options = kNullPrototype) {
   return collectSync(source, options.limit);
 }

+/**
+ * Read a sync source to completion, discarding every chunk.
+ * @param {Iterable<Uint8Array[]>} source
+ * @param {{ limit?: number }} [options]
+ * @returns {undefined}
+ */
+function dumpSync(source, options = kNullPrototype) {
+  options = converters.ConsumeSyncOptions(options, {
+    __proto__: null,
+    context: 'options',
+  });
+
+  const limit = options.limit;
+  const normalized = fromSync(source);
+  let totalBytes = 0;
+
+  for (const batch of normalized) {
+    // With no limit, just iterate through the stream completely.
+    if (limit === undefined) continue;
+    // Otherwise, calculate totalBytes and track the limit.
+    for (let i = 0; i < batch.length; i++) {
+      totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
+      if (totalBytes > limit) {
+        throw new ERR_OUT_OF_RANGE('totalBytes', `<= ${limit}`, totalBytes);
+      }
+    }
+  }
+}
+
 // =============================================================================
 // Async Consumers
 // =============================================================================
@@ -345,6 +375,43 @@ async function array(source, options = kNullPrototype) {
   return collectAsync(source, options.signal, options.limit);
 }

+/**
+ * Read an async or sync source to completion, discarding every chunk.
+ * @param {AsyncIterable<Uint8Array[]>|Iterable<Uint8Array[]>} source
+ * @param {{ signal?: AbortSignal, limit?: number }} [options]
+ * @returns {Promise<undefined>}
+ */
+async function dump(source, options = kNullPrototype) {
+  options = converters.ConsumeOptions(options, {
+    __proto__: null,
+    context: 'options',
+  });
+  const signal = options.signal;
+  const limit = options.limit;
+
+  signal?.throwIfAborted();
+
+  const abortableSource = signal && isAsyncIterable(source) ?
+    yieldAbortable(source, signal) : source;
+  const normalized = from(abortableSource);
+  const iterable = signal ? yieldAbortable(normalized, signal) : normalized;
+
+  let totalBytes = 0;
+
+  for await (const batch of iterable) {
+    signal?.throwIfAborted();
+    // With no limit, just iterate through the stream completely.
+    if (limit === undefined) continue;
+    // Otherwise, calculate totalBytes and track the limit.
+    for (let i = 0; i < batch.length; i++) {
+      totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
+      if (totalBytes > limit) {
+        throw new ERR_OUT_OF_RANGE('totalBytes', `<= ${limit}`, totalBytes);
+      }
+    }
+  }
+}
+
 // =============================================================================
 // Tap Utilities
 // =============================================================================
@@ -610,6 +677,8 @@ module.exports = {
   arraySync,
   bytes,
   bytesSync,
+  dump,
+  dumpSync,
   merge,
   ondrain,
   tap,
diff --git a/lib/stream/iter.js b/lib/stream/iter.js
index 4cb499bb161..9e2a86b63c1 100644
--- a/lib/stream/iter.js
+++ b/lib/stream/iter.js
@@ -44,6 +44,8 @@ const {
   arrayBufferSync,
   array,
   arraySync,
+  dump,
+  dumpSync,
   tap,
   tapSync,
   merge,
@@ -100,12 +102,14 @@ const Stream = ObjectFreeze({
   text,
   arrayBuffer,
   array,
+  dump,

   // Consumers (sync)
   bytesSync,
   textSync,
   arrayBufferSync,
   arraySync,
+  dumpSync,

   // Combining
   merge,
@@ -164,12 +168,14 @@ module.exports = {
   text,
   arrayBuffer,
   array,
+  dump,

   // Consumers (sync)
   bytesSync,
   textSync,
   arrayBufferSync,
   arraySync,
+  dumpSync,

   // Combining
   merge,
diff --git a/test/parallel/test-stream-iter-consumers-dump.mjs b/test/parallel/test-stream-iter-consumers-dump.mjs
new file mode 100644
index 00000000000..4bbee22a143
--- /dev/null
+++ b/test/parallel/test-stream-iter-consumers-dump.mjs
@@ -0,0 +1,424 @@
+// Flags: --experimental-stream-iter --expose-gc
+
+import { mustCall } from '../common/index.mjs';
+import { createRequire } from 'node:module';
+import assert from 'node:assert';
+import {
+  array,
+  dump,
+  dumpSync,
+  from,
+  fromSync,
+  pull,
+  push,
+  tap,
+  toAsyncStreamable,
+} from 'node:stream/iter';
+
+const require = createRequire(import.meta.url);
+const { gcUntil } = require('../common/gc');
+
+// =============================================================================
+// dump
+// =============================================================================
+
+async function testDumpResolvesUndefined() {
+  assert.strictEqual(await dump(from('hello')), undefined);
+}
+
+async function testDumpConsumesToCompletion() {
+  let pulls = 0;
+  async function* gen() {
+    for (let i = 0; i < 5; i++) {
+      pulls++;
+      yield [new Uint8Array([i])];
+    }
+  }
+  await dump(gen());
+  assert.strictEqual(pulls, 5);
+}
+
+async function testDumpEmpty() {
+  assert.strictEqual(await dump(from([])), undefined);
+  async function* empty() {}
+  assert.strictEqual(await dump(empty()), undefined);
+}
+
+async function testDumpStringSource() {
+  // Sources are normalized via from(), so raw ByteInput works.
+  assert.strictEqual(await dump('a direct string'), undefined);
+}
+
+async function testDumpSyncSourceViaAsync() {
+  // dump() accepts sync iterables too, like the other async consumers.
+  assert.strictEqual(await dump(fromSync('sync source')), undefined);
+}
+
+async function testDumpAlreadyConsumed() {
+  // A source that has already been read to completion drains to a no-op
+  // rather than throwing.
+  async function* gen() {
+    yield [new Uint8Array([1])];
+  }
+  const source = gen();
+  await dump(source);
+  assert.strictEqual(await dump(source), undefined);
+}
+
+// =============================================================================
+// dump: error propagation
+// =============================================================================
+
+async function testDumpRejectsOnMidStreamError() {
+  const boom = new Error('boom');
+  async function* failing() {
+    yield [new Uint8Array([1])];
+    yield [new Uint8Array([2])];
+    throw boom;
+  }
+  await assert.rejects(dump(failing()), boom);
+}
+
+async function testDumpRejectsOnFirstChunkError() {
+  const boom = new Error('immediate boom');
+  async function* failing() {
+    await Promise.reject(boom);
+    yield [];
+  }
+  await assert.rejects(dump(failing()), boom);
+}
+
+async function testDumpRejectsInvalidChunkType() {
+  async function* bad() {
+    yield [42];
+  }
+  await assert.rejects(dump(bad()), { code: 'ERR_INVALID_ARG_TYPE' });
+}
+
+async function testDumpCallsReturnOnError() {
+  // An abrupt completion must still release the source.
+  let returned = false;
+  const boom = new Error('cleanup boom');
+  const source = {
+    __proto__: null,
+    async *[Symbol.asyncIterator]() {
+      try {
+        yield [new Uint8Array([1])];
+        throw boom;
+      } finally {
+        returned = true;
+      }
+    },
+  };
+  await assert.rejects(dump(source), boom);
+  assert.strictEqual(returned, true);
+}
+
+// =============================================================================
+// dump: AbortSignal
+// =============================================================================
+
+async function testDumpAlreadyAbortedSignal() {
+  await assert.rejects(
+    () => dump(from('data'), { signal: AbortSignal.abort() }),
+    { name: 'AbortError' },
+  );
+}
+
+async function testDumpAbortsPendingNext() {
+  const ac = new AbortController();
+  const reason = new Error('dump abort');
+
+  async function* never() {
+    await new Promise(() => {});
+    yield [];
+  }
+
+  const promise = dump(never(), { __proto__: null, signal: ac.signal });
+  ac.abort(reason);
+
+  await assert.rejects(promise, reason);
+}
+
+async function testDumpAbortsPendingNormalization() {
+  const ac = new AbortController();
+  const reason = new Error('dump normalization abort');
+  const source = {
+    __proto__: null,
+    [toAsyncStreamable]() {
+      return new Promise(() => {});
+    },
+  };
+
+  const promise = dump(source, { __proto__: null, signal: ac.signal });
+  ac.abort(reason);
+
+  await assert.rejects(promise, reason);
+}
+
+async function testDumpUnabortedSignalCompletes() {
+  const ac = new AbortController();
+  assert.strictEqual(
+    await dump(from('finishes first'), { signal: ac.signal }),
+    undefined,
+  );
+}
+
+// =============================================================================
+// dump: limit
+// =============================================================================
+
+async function testDumpLimitRejects() {
+  async function* gen() {
+    yield [new Uint8Array(100)];
+    yield [new Uint8Array(100)];
+  }
+  await assert.rejects(
+    () => dump(gen(), { limit: 150 }),
+    { name: 'RangeError', code: 'ERR_OUT_OF_RANGE' },
+  );
+}
+
+async function testDumpLimitAllowsExact() {
+  async function* gen() {
+    yield [new Uint8Array(50)];
+    yield [new Uint8Array(50)];
+  }
+  assert.strictEqual(await dump(gen(), { limit: 100 }), undefined);
+}
+
+async function testDumpLimitCancelsSource() {
+  // Exceeding the limit is an abrupt completion, so the source must be
+  // released rather than left half-read.
+  let returned = false;
+  const source = {
+    __proto__: null,
+    async *[Symbol.asyncIterator]() {
+      try {
+        while (true) yield [new Uint8Array(64)];
+      } finally {
+        returned = true;
+      }
+    },
+  };
+  await assert.rejects(() => dump(source, { limit: 128 }),
+                       { code: 'ERR_OUT_OF_RANGE' });
+  assert.strictEqual(returned, true);
+}
+
+async function testDumpLimitIsOptIn() {
+  // No default limit: a source larger than undici's 128 KB dump() default
+  // must still be read to completion.
+  async function* big() {
+    for (let i = 0; i < 64; i++) yield [new Uint8Array(8192)];
+  }
+  assert.strictEqual(await dump(big()), undefined);
+}
+
+// =============================================================================
+// dump: does not retain data
+// =============================================================================
+
+async function testDumpDoesNotRetain() {
+  // Allocate a distinct buffer per chunk and keep only weak references to
+  // them. After the consumer finishes, anything it retained is still
+  // reachable; anything it discarded is collectable.
+  const kChunks = 64;
+
+  async function measure(consumer, keepResult) {
+    const refs = [];
+    async function* source() {
+      for (let i = 0; i < kChunks; i++) {
+        const buf = new Uint8Array(64 * 1024);
+        refs.push(new WeakRef(buf));
+        yield [buf];
+      }
+    }
+    const result = await consumer(source());
+    let live = kChunks;
+    const settled = await gcUntil('dump retention', () => {
+      live = refs.filter((ref) => ref.deref() !== undefined).length;
+      return live === 0;
+    }).then(() => true, () => false);
+    // Keep the collecting consumer's result reachable across the GC, so its
+    // retention is what is being measured rather than the result being dropped.
+    if (keepResult) assert.strictEqual(result.length, kChunks);
+    return { settled, live };
+  }
+
+  // dump() keeps nothing, so every chunk becomes collectable.
+  const drained = await measure(dump, false);
+  assert.ok(drained.settled,
+            `dump() retained ${drained.live}/${kChunks} chunks`);
+
+  // Positive control: array() does retain, which proves the measurement above
+  // is capable of observing retention rather than always passing.
+  const collected = await measure(array, true);
+  assert.ok(collected.live === kChunks,
+            `array() should retain every chunk, kept ${collected.live}`);
+}
+
+async function testDumpReleasesBackpressure() {
+  // Consuming is what returns budget to the writer. With a small budget the
+  // writer blocks until dump() pulls, so this only completes if dump()
+  // really is reading.
+  const { writer, readable } = push({ budget: 16384 });
+  const chunk = new Uint8Array(8192);
+
+  const draining = dump(readable);
+
+  for (let i = 0; i < 32; i++) {
+    await writer.write(chunk);
+  }
+  await writer.end();
+
+  assert.strictEqual(await draining, undefined);
+}
+
+// =============================================================================
+// dump: interaction with transforms
+// =============================================================================
+
+async function testDumpWithTap() {
+  // The documented pattern: observe via tap() while retaining nothing.
+  let seenBytes = 0;
+  let seenBatches = 0;
+  const counter = tap((chunks) => {
+    // tap() invokes the callback once more with null to signal end of stream.
+    if (chunks === null) return;
+    seenBatches++;
+    for (const chunk of chunks) seenBytes += chunk.byteLength;
+  });
+
+  await dump(pull(from('hello world'), counter));
+  assert.strictEqual(seenBytes, 11);
+  assert.ok(seenBatches > 0);
+}
+
+async function testDumpPropagatesTransformError() {
+  const boom = new Error('transform boom');
+  const badTransform = () => { throw boom; };
+  await assert.rejects(dump(pull(from('hello'), badTransform)), boom);
+}
+
+// =============================================================================
+// dumpSync
+// =============================================================================
+
+function testDumpSyncBasic() {
+  assert.strictEqual(dumpSync(fromSync('hello')), undefined);
+}
+
+function testDumpSyncConsumesToCompletion() {
+  let pulls = 0;
+  function* gen() {
+    for (let i = 0; i < 5; i++) {
+      pulls++;
+      yield new Uint8Array([i]);
+    }
+  }
+  dumpSync(gen());
+  assert.strictEqual(pulls, 5);
+}
+
+function testDumpSyncEmpty() {
+  assert.strictEqual(dumpSync(fromSync([])), undefined);
+  function* empty() {}
+  assert.strictEqual(dumpSync(empty()), undefined);
+}
+
+function testDumpSyncStringSource() {
+  assert.strictEqual(dumpSync('direct sync string'), undefined);
+}
+
+function testDumpSyncThrowsMidStream() {
+  const boom = new Error('sync boom');
+  function* failing() {
+    yield new Uint8Array([1]);
+    throw boom;
+  }
+  assert.throws(() => dumpSync(failing()), boom);
+}
+
+function testDumpSyncRejectsAsyncSource() {
+  // Same contract as the other *Sync consumers: an async-only source is a
+  // type error, not a silent no-op.
+  async function* asyncGen() {
+    yield [new Uint8Array([1])];
+  }
+  assert.throws(() => dumpSync(asyncGen()),
+                { code: 'ERR_INVALID_ARG_TYPE' });
+  assert.throws(() => dumpSync(Promise.resolve('x')),
+                { code: 'ERR_INVALID_ARG_TYPE' });
+}
+
+function testDumpSyncLimit() {
+  function* gen() {
+    yield new Uint8Array(100);
+    yield new Uint8Array(100);
+  }
+  assert.throws(
+    () => dumpSync(gen(), { limit: 150 }),
+    { name: 'RangeError', code: 'ERR_OUT_OF_RANGE' },
+  );
+  assert.strictEqual(dumpSync(gen(), { limit: 200 }), undefined);
+}
+
+// =============================================================================
+// Option validation
+// =============================================================================
+
+async function testDumpOptionValidation() {
+  await assert.rejects(() => dump(from('a'), 42),
+                       { code: 'ERR_INVALID_ARG_TYPE' });
+  await assert.rejects(() => dump(from('a'), { signal: 'bad' }),
+                       { code: 'ERR_INVALID_ARG_TYPE' });
+  await assert.rejects(() => dump(from('a'), { limit: 'bad' }),
+                       { code: 'ERR_INVALID_ARG_TYPE' });
+  await assert.rejects(() => dump(from('a'), { limit: -1 }),
+                       { code: 'ERR_OUT_OF_RANGE' });
+}
+
+function testDumpSyncOptionValidation() {
+  assert.throws(() => dumpSync(fromSync('a'), 42),
+                { code: 'ERR_INVALID_ARG_TYPE' });
+  assert.throws(() => dumpSync(fromSync('a'), { limit: 'bad' }),
+                { code: 'ERR_INVALID_ARG_TYPE' });
+  assert.throws(() => dumpSync(fromSync('a'), { limit: -1 }),
+                { code: 'ERR_OUT_OF_RANGE' });
+}
+
+testDumpSyncBasic();
+testDumpSyncConsumesToCompletion();
+testDumpSyncEmpty();
+testDumpSyncStringSource();
+testDumpSyncThrowsMidStream();
+testDumpSyncRejectsAsyncSource();
+testDumpSyncLimit();
+testDumpSyncOptionValidation();
+
+await Promise.all([
+  testDumpResolvesUndefined(),
+  testDumpConsumesToCompletion(),
+  testDumpEmpty(),
+  testDumpStringSource(),
+  testDumpSyncSourceViaAsync(),
+  testDumpAlreadyConsumed(),
+  testDumpRejectsOnMidStreamError(),
+  testDumpRejectsOnFirstChunkError(),
+  testDumpRejectsInvalidChunkType(),
+  testDumpCallsReturnOnError(),
+  testDumpAlreadyAbortedSignal(),
+  testDumpAbortsPendingNext(),
+  testDumpAbortsPendingNormalization(),
+  testDumpUnabortedSignalCompletes(),
+  testDumpLimitRejects(),
+  testDumpLimitAllowsExact(),
+  testDumpLimitCancelsSource(),
+  testDumpLimitIsOptIn(),
+  testDumpDoesNotRetain(),
+  testDumpReleasesBackpressure(),
+  testDumpWithTap(),
+  testDumpPropagatesTransformError(),
+  testDumpOptionValidation(),
+]).then(mustCall());
diff --git a/test/parallel/test-stream-iter-namespace.js b/test/parallel/test-stream-iter-namespace.js
index ce197e85846..8e437de3831 100644
--- a/test/parallel/test-stream-iter-namespace.js
+++ b/test/parallel/test-stream-iter-namespace.js
@@ -43,6 +43,7 @@ async function testStreamNamespaceAsyncConsumers() {
   assert.strictEqual(typeof Stream.text, 'function');
   assert.strictEqual(typeof Stream.arrayBuffer, 'function');
   assert.strictEqual(typeof Stream.array, 'function');
+  assert.strictEqual(typeof Stream.dump, 'function');
 }

 async function testStreamNamespaceSyncConsumers() {
@@ -52,6 +53,7 @@ async function testStreamNamespaceSyncConsumers() {
   assert.strictEqual(typeof Stream.textSync, 'function');
   assert.strictEqual(typeof Stream.arrayBufferSync, 'function');
   assert.strictEqual(typeof Stream.arraySync, 'function');
+  assert.strictEqual(typeof Stream.dumpSync, 'function');
 }

 async function testStreamNamespaceCombining() {
@@ -108,6 +110,8 @@ async function testIndividualExports() {
   assert.strictEqual(typeof streamNew.arrayBufferSync, 'function');
   assert.strictEqual(typeof streamNew.array, 'function');
   assert.strictEqual(typeof streamNew.arraySync, 'function');
+  assert.strictEqual(typeof streamNew.dump, 'function');
+  assert.strictEqual(typeof streamNew.dumpSync, 'function');

   // Combining
   assert.strictEqual(typeof streamNew.merge, 'function');
@@ -163,6 +167,8 @@ async function testNamespaceMatchesExports() {
   assert.strictEqual(Stream.textSync, streamNew.textSync);
   assert.strictEqual(Stream.arrayBufferSync, streamNew.arrayBufferSync);
   assert.strictEqual(Stream.arraySync, streamNew.arraySync);
+  assert.strictEqual(Stream.dump, streamNew.dump);
+  assert.strictEqual(Stream.dumpSync, streamNew.dumpSync);
   assert.strictEqual(Stream.merge, streamNew.merge);
   assert.strictEqual(Stream.broadcast, streamNew.broadcast);
   assert.strictEqual(Stream.share, streamNew.share);