Commit 85bd8607038 for nodejs

commit 85bd8607038dc5bcbe444fd005cdecf4320bf1f0
Author: James M Snell <jasnell@gmail.com>
Date:   Sun Aug 30 17:13:53 2026 +0000

    stream: ensure streamable protocols precede fast paths

    Signed-off-by: James M Snell <jasnell@gmail.com>
    Assisted-by: Opencode
    PR-URL: https://github.com/nodejs/node/pull/66030
    Reviewed-By: Filip Skokan <panva.ip@gmail.com>
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/lib/internal/streams/iter/from.js b/lib/internal/streams/iter/from.js
index 0d95b813e01..cac8fd3565b 100644
--- a/lib/internal/streams/iter/from.js
+++ b/lib/internal/streams/iter/from.js
@@ -661,6 +661,13 @@ function fromSync(input) {
     };
   }

+  // Check toStreamable protocol (takes precedence over iteration protocols).
+  // toAsyncStreamable is ignored entirely in fromSync.
+  const streamableMethod = getProtocolMethod(input, toStreamable);
+  if (streamableMethod !== undefined) {
+    return fromSync(FunctionPrototypeCall(streamableMethod, input));
+  }
+
   // Fast path: Uint8Array[] - yield in bounded sub-batches.
   // Yielding the entire array as one batch forces downstream transforms
   // to process all data at once, causing peak memory proportional to total
@@ -696,13 +703,6 @@ function fromSync(input) {
     }
   }

-  // Check toStreamable protocol (takes precedence over iteration protocols).
-  // toAsyncStreamable is ignored entirely in fromSync.
-  const streamableMethod = getProtocolMethod(input, toStreamable);
-  if (streamableMethod !== undefined) {
-    return fromSync(FunctionPrototypeCall(streamableMethod, input));
-  }
-
   const isIterable = isSyncIterable(input);

   // Reject explicit async-only inputs
@@ -751,11 +751,6 @@ function from(input) {
     throw new ERR_INVALID_ARG_TYPE('input', 'a non-null value', input);
   }

-  // Fast path: validated source already yields valid Uint8Array[] batches
-  if (input[kValidatedSource]) {
-    return input;
-  }
-
   // Check for primitives first (ByteInput)
   if (isPrimitiveChunk(input)) {
     const chunk = primitiveToUint8Array(input);
@@ -767,6 +762,34 @@ function from(input) {
     };
   }

+  // Check toAsyncStreamable protocol (takes precedence over toStreamable and
+  // iteration protocols)
+  const asyncStreamableMethod = getProtocolMethod(input, toAsyncStreamable);
+  if (asyncStreamableMethod !== undefined) {
+    let result = FunctionPrototypeCall(asyncStreamableMethod, input);
+    if (isPromise(result)) {
+      result = PromisePrototypeThen(result, undefined, undefined);
+      markPromiseAsHandled(result);
+    }
+    // Synchronous validated source (e.g. Readable batched iterator)
+    if (result?.[kValidatedSource]) {
+      return result;
+    }
+    return createNormalizationSource(
+      (context) => normalizeAsyncStreamableResult(result, context));
+  }
+
+  // Check toStreamable protocol (takes precedence over iteration protocols)
+  const streamableMethod = getProtocolMethod(input, toStreamable);
+  if (streamableMethod !== undefined) {
+    return from(FunctionPrototypeCall(streamableMethod, input));
+  }
+
+  // Fast path: validated source already yields valid Uint8Array[] batches
+  if (input[kValidatedSource]) {
+    return input;
+  }
+
   // Fast path: Uint8Array[] - yield in bounded sub-batches.
   // Yielding the entire array as one batch forces downstream transforms
   // to process all data at once, causing peak memory proportional to total
@@ -801,29 +824,6 @@ function from(input) {
     }
   }

-  // Check toAsyncStreamable protocol (takes precedence over toStreamable and
-  // iteration protocols)
-  const asyncStreamableMethod = getProtocolMethod(input, toAsyncStreamable);
-  if (asyncStreamableMethod !== undefined) {
-    let result = FunctionPrototypeCall(asyncStreamableMethod, input);
-    if (isPromise(result)) {
-      result = PromisePrototypeThen(result, undefined, undefined);
-      markPromiseAsHandled(result);
-    }
-    // Synchronous validated source (e.g. Readable batched iterator)
-    if (result?.[kValidatedSource]) {
-      return result;
-    }
-    return createNormalizationSource(
-      (context) => normalizeAsyncStreamableResult(result, context));
-  }
-
-  // Check toStreamable protocol (takes precedence over iteration protocols)
-  const streamableMethod = getProtocolMethod(input, toStreamable);
-  if (streamableMethod !== undefined) {
-    return from(FunctionPrototypeCall(streamableMethod, input));
-  }
-
   // Must be a Streamable (sync or async iterable)
   if (!isSyncIterable(input) && !isAsyncIterable(input)) {
     throw new ERR_INVALID_ARG_TYPE(
diff --git a/test/parallel/test-stream-iter-property-access.js b/test/parallel/test-stream-iter-property-access.js
index 344a481b081..5ba1a5a587f 100644
--- a/test/parallel/test-stream-iter-property-access.js
+++ b/test/parallel/test-stream-iter-property-access.js
@@ -3,6 +3,7 @@

 const common = require('../common');
 const assert = require('assert');
+const { Readable } = require('stream');
 const {
   Broadcast,
   Share,
@@ -21,13 +22,14 @@ const {
   shareProtocol,
   shareSync,
   shareSyncProtocol,
+  text,
+  textSync,
   toAsyncStreamable,
   toStreamable,
 } = require('stream/iter');

-function protocolFixture(symbol, result) {
+function protocolFixture(symbol, result, input = {}) {
   let accesses = 0;
-  const input = {};
   const method = common.mustCall(function() {
     assert.strictEqual(this, input);
     return result;
@@ -83,6 +85,38 @@ async function testFromSyncSnapshotsProtocolMethods() {
   assert.strictEqual(fixture.accesses, 1);
 }

+async function testArrayFastPathsHonorProtocols() {
+  const asyncInputs = [
+    protocolFixture(toAsyncStreamable, 'empty-async', []),
+    protocolFixture(toStreamable, 'batch-async', [new Uint8Array([0])]),
+  ];
+  for (const fixture of asyncInputs) {
+    assert.match(await text(from(fixture.input)), /-async$/);
+    assert.strictEqual(fixture.accesses, 1);
+  }
+
+  const syncInputs = [
+    protocolFixture(toStreamable, 'empty-sync', []),
+    protocolFixture(toStreamable, 'batch-sync', [new Uint8Array([0])]),
+  ];
+  for (const fixture of syncInputs) {
+    assert.match(textSync(fromSync(fixture.input)), /-sync$/);
+    assert.strictEqual(fixture.accesses, 1);
+  }
+}
+
+async function testValidatedSourceHonorsProtocol() {
+  const readable = Readable.from(['ignored']);
+  const validated = readable[toAsyncStreamable]();
+  assert.strictEqual(from(validated), validated);
+
+  const fixture = protocolFixture(
+    toAsyncStreamable, 'validated-protocol', validated);
+  assert.strictEqual(await text(from(fixture.input)), 'validated-protocol');
+  assert.strictEqual(fixture.accesses, 1);
+  readable.destroy();
+}
+
 async function testPullSnapshotsStatefulTransform() {
   const fixture = statefulTransformFixture();
   const controller = new AbortController();
@@ -136,6 +170,8 @@ async function testDrainableProtocolSnapshotMethod() {
 Promise.all([
   testFromSnapshotsProtocolMethods(),
   testFromSyncSnapshotsProtocolMethods(),
+  testArrayFastPathsHonorProtocols(),
+  testValidatedSourceHonorsProtocol(),
   testPullSnapshotsStatefulTransform(),
   testPullSyncSnapshotsStatefulTransform(),
   testMultiConsumerProtocolsSnapshotMethods(),