Commit 5bcade91dee for nodejs
commit 5bcade91deeb5579e71e8d159bc548018129f00f
Author: James M Snell <jasnell@gmail.com>
Date: Sun Oct 4 01:34:53 2026 +0000
stream: make endSync() optional in pipeToSync()
pipeToSync() threw ERR_INVALID_ARG_TYPE before writing anything when
the writer had no endSync() method and preventClose was not set.
endSync() is optional: the spec (pipeToSync() step 7) only calls it if
the writer has it, and pipeTo() already treats it that way. A writer
without endSync() now receives the data and is not closed. pipeToSync()
still never falls back to the async end().
This also fixes the from-sync-writev case of
benchmark/streams/iter-from-batching.js, whose writer has no endSync().
testPipeToSyncNoEndSync asserted the previous rejection and now checks
that the data is written and end() is not called. The documentation of
the writer requirements is corrected as well: only writeSync() is
required.
Assisted-by: OpenCode
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66648
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
diff --git a/doc/api/stream_iter.md b/doc/api/stream_iter.md
index aa96ce02813..e2f30ce6a94 100644
--- a/doc/api/stream_iter.md
+++ b/doc/api/stream_iter.md
@@ -714,7 +714,7 @@ added:
* `source` {Iterable} The sync data source.
* `...transforms` {Function|Object} Zero or more sync transforms.
-* `writer` {Object} Destination with `write(chunk)` method.
+* `writer` {Object} Destination with a `writeSync(chunk)` method.
* `options` {Object}
* `failOnIncompleteClose` {boolean} If `true`, call `writer.fail()` when
`writer.endSync()` cannot close the writer synchronously. Ignored when
@@ -727,8 +727,11 @@ added:
Synchronous version of [`pipeTo()`][]. The `source`, all transforms, and the
`writer` must be synchronous. Cannot accept async iterables or promises.
-The `writer` must have the `*Sync` methods (`writeSync`, `writevSync`,
-`endSync`) and `fail()` for this to work.
+The `writer` must have a `writeSync()` method. The other methods are
+optional: `writevSync()` is used for batches of more than one chunk if it is
+present, `endSync()` is called to close the writer (unless `preventClose` is
+`true`), and `fail()` is called if the pipe fails (unless `preventFail` is
+`true`). A writer without `endSync()` is not closed.
`pipeToSync()` never falls back to the asynchronous writer methods. If
`writer.endSync()` returns `-1` because the writer cannot close synchronously
diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js
index 6dff97e42be..70e89a99423 100644
--- a/lib/internal/streams/iter/pull.js
+++ b/lib/internal/streams/iter/pull.js
@@ -1032,12 +1032,9 @@ function pipeToSync(source, ...args) {
context: 'options',
});
const hasWritevSync = typeof writer.writevSync === 'function';
+ // endSync() is optional: a writer without it is not closed.
const endSync = writer.endSync;
-
- if (!options.preventClose && typeof endSync !== 'function') {
- throw new ERR_INVALID_ARG_TYPE(
- 'writer.endSync', 'Function', endSync);
- }
+ const hasEndSync = typeof endSync === 'function';
// Normalize source and create pipeline
const normalized = fromSync(source);
@@ -1074,7 +1071,7 @@ function pipeToSync(source, ...args) {
}
}
- if (!options.preventClose) {
+ if (!options.preventClose && hasEndSync) {
closedSync = FunctionPrototypeCall(endSync, writer) >= 0;
}
} catch (error) {
diff --git a/test/parallel/test-stream-iter-pipeto-edge.js b/test/parallel/test-stream-iter-pipeto-edge.js
index 0c5448db5a8..082871a9e4c 100644
--- a/test/parallel/test-stream-iter-pipeto-edge.js
+++ b/test/parallel/test-stream-iter-pipeto-edge.js
@@ -47,20 +47,18 @@ async function testPipeToSyncEndSyncFailureDoesNotFailWriter() {
assert.strictEqual(await result, 'abcdef');
}
-// pipeToSync requires endSync() when closing is enabled.
+// pipeToSync does not require endSync().
async function testPipeToSyncNoEndSync() {
- let writeCalled = false;
- let endCalled = false;
+ // endSync() is optional. Without it the data is still written and the
+ // writer is not closed; pipeToSync() never falls back to end().
+ const written = [];
const writer = {
- writeSync() { writeCalled = true; return true; },
- end() { endCalled = true; },
+ writeSync(chunk) { written.push(chunk); return true; },
+ end: common.mustNotCall(),
+ fail: common.mustNotCall(),
};
- assert.throws(
- () => pipeToSync(fromSync('data'), writer),
- { code: 'ERR_INVALID_ARG_TYPE' },
- );
- assert.strictEqual(writeCalled, false);
- assert.strictEqual(endCalled, false);
+ assert.strictEqual(pipeToSync(fromSync('data'), writer), 4);
+ assert.deepStrictEqual(written, [new TextEncoder().encode('data')]);
}
// pipeToSync with preventFail: true — source error does NOT call fail()