Bound sync iterable normalization in from() and fromSync() to FROM_BATCH_SIZE. This avoids unbounded batches for from() sync iterable fallbacks and lets fromSync() coalesce plain Uint8Array values for writev paths. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63324 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
237 lines
6.9 KiB
JavaScript
237 lines
6.9 KiB
JavaScript
// Flags: --experimental-stream-iter
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { fromSync } = require('stream/iter');
|
|
|
|
function testFromSyncString() {
|
|
// String input should be UTF-8 encoded
|
|
const readable = fromSync('hello');
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.strictEqual(batches[0].length, 1);
|
|
assert.deepStrictEqual(batches[0][0],
|
|
new TextEncoder().encode('hello'));
|
|
}
|
|
|
|
function testFromSyncUint8Array() {
|
|
const input = new Uint8Array([1, 2, 3]);
|
|
const readable = fromSync(input);
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.strictEqual(batches[0].length, 1);
|
|
assert.deepStrictEqual(batches[0][0], input);
|
|
}
|
|
|
|
function testFromSyncArrayBuffer() {
|
|
const ab = new ArrayBuffer(4);
|
|
new Uint8Array(ab).set([10, 20, 30, 40]);
|
|
const readable = fromSync(ab);
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.deepStrictEqual(batches[0][0], new Uint8Array([10, 20, 30, 40]));
|
|
}
|
|
|
|
function testFromSyncUint8ArrayArray() {
|
|
// Array of Uint8Array should yield as a single batch
|
|
const chunks = [new Uint8Array([1]), new Uint8Array([2])];
|
|
const readable = fromSync(chunks);
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.strictEqual(batches[0].length, 2);
|
|
assert.deepStrictEqual(batches[0][0], new Uint8Array([1]));
|
|
assert.deepStrictEqual(batches[0][1], new Uint8Array([2]));
|
|
}
|
|
|
|
function testFromSyncGenerator() {
|
|
function* gen() {
|
|
yield new Uint8Array([1, 2]);
|
|
yield new Uint8Array([3, 4]);
|
|
}
|
|
const readable = fromSync(gen());
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.strictEqual(batches[0].length, 2);
|
|
assert.deepStrictEqual(batches[0][0], new Uint8Array([1, 2]));
|
|
assert.deepStrictEqual(batches[0][1], new Uint8Array([3, 4]));
|
|
}
|
|
|
|
function testFromSyncNestedIterables() {
|
|
// Nested arrays and strings should be flattened
|
|
function* gen() {
|
|
yield ['hello', ' ', 'world'];
|
|
}
|
|
const readable = fromSync(gen());
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.strictEqual(batches[0].length, 3);
|
|
assert.deepStrictEqual(batches[0][0], new TextEncoder().encode('hello'));
|
|
assert.deepStrictEqual(batches[0][1], new TextEncoder().encode(' '));
|
|
assert.deepStrictEqual(batches[0][2], new TextEncoder().encode('world'));
|
|
}
|
|
|
|
function testFromSyncToStreamableProtocol() {
|
|
const sym = Symbol.for('Stream.toStreamable');
|
|
const obj = {
|
|
[sym]() {
|
|
return 'protocol-data';
|
|
},
|
|
};
|
|
function* gen() {
|
|
yield obj;
|
|
}
|
|
const readable = fromSync(gen());
|
|
const batches = [];
|
|
for (const batch of readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.deepStrictEqual(batches[0][0],
|
|
new TextEncoder().encode('protocol-data'));
|
|
}
|
|
|
|
function testFromSyncGeneratorError() {
|
|
function* gen() {
|
|
yield new Uint8Array([1]);
|
|
throw new Error('generator boom');
|
|
}
|
|
const readable = fromSync(gen());
|
|
assert.throws(() => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const _ of readable) { /* consume */ }
|
|
}, { message: 'generator boom' });
|
|
}
|
|
|
|
function testFromSyncRejectsNonStreamable() {
|
|
assert.throws(
|
|
() => fromSync(12345),
|
|
{ code: 'ERR_INVALID_ARG_TYPE' },
|
|
);
|
|
assert.throws(
|
|
() => fromSync(null),
|
|
{ code: 'ERR_INVALID_ARG_TYPE' },
|
|
);
|
|
}
|
|
|
|
function testFromSyncEmptyGenerator() {
|
|
function* empty() {}
|
|
let count = 0;
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const _ of fromSync(empty())) { count++; }
|
|
assert.strictEqual(count, 0);
|
|
}
|
|
|
|
// Top-level toStreamable protocol on input to fromSync()
|
|
function testFromSyncTopLevelToStreamable() {
|
|
const obj = {
|
|
[Symbol.for('Stream.toStreamable')]() {
|
|
return 'top-level-sync';
|
|
},
|
|
};
|
|
const batches = [];
|
|
for (const batch of fromSync(obj)) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.deepStrictEqual(batches[0][0],
|
|
new TextEncoder().encode('top-level-sync'));
|
|
}
|
|
|
|
// Top-level: toStreamable takes precedence over Symbol.iterator
|
|
function testFromSyncTopLevelProtocolOverIterator() {
|
|
const obj = {
|
|
[Symbol.for('Stream.toStreamable')]() { return 'from-protocol'; },
|
|
*[Symbol.iterator]() { yield [new TextEncoder().encode('from-iterator')]; },
|
|
};
|
|
const batches = [];
|
|
for (const batch of fromSync(obj)) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.deepStrictEqual(batches[0][0],
|
|
new TextEncoder().encode('from-protocol'));
|
|
}
|
|
|
|
// Top-level: toAsyncStreamable is ignored by fromSync
|
|
function testFromSyncIgnoresAsyncStreamable() {
|
|
const obj = {
|
|
[Symbol.for('Stream.toAsyncStreamable')]() { return 'async'; },
|
|
};
|
|
// Has no toStreamable and no Symbol.iterator, should throw
|
|
assert.throws(() => fromSync(obj), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
}
|
|
|
|
// Explicit async iterable rejected
|
|
function testFromSyncRejectsAsyncIterable() {
|
|
async function* gen() { yield [new TextEncoder().encode('a')]; }
|
|
assert.throws(() => fromSync(gen()), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
}
|
|
|
|
// Promise rejected
|
|
function testFromSyncRejectsPromise() {
|
|
assert.throws(() => fromSync(Promise.resolve('hello')),
|
|
{ code: 'ERR_INVALID_ARG_TYPE' });
|
|
}
|
|
|
|
// DataView input should be converted to Uint8Array (zero-copy)
|
|
function testFromSyncDataView() {
|
|
const buf = new ArrayBuffer(3);
|
|
const view = new DataView(buf);
|
|
view.setUint8(0, 0x48); // H
|
|
view.setUint8(1, 0x49); // I
|
|
view.setUint8(2, 0x21); // !
|
|
const batches = [];
|
|
for (const batch of fromSync(view)) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 1);
|
|
assert.deepStrictEqual(batches[0][0], new Uint8Array([0x48, 0x49, 0x21]));
|
|
}
|
|
|
|
function testFromSyncNullThrows() {
|
|
assert.throws(() => fromSync(null), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
}
|
|
|
|
function testFromSyncUndefinedThrows() {
|
|
assert.throws(() => fromSync(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
}
|
|
|
|
Promise.all([
|
|
testFromSyncString(),
|
|
testFromSyncUint8Array(),
|
|
testFromSyncArrayBuffer(),
|
|
testFromSyncUint8ArrayArray(),
|
|
testFromSyncGenerator(),
|
|
testFromSyncNestedIterables(),
|
|
testFromSyncToStreamableProtocol(),
|
|
testFromSyncGeneratorError(),
|
|
testFromSyncRejectsNonStreamable(),
|
|
testFromSyncEmptyGenerator(),
|
|
testFromSyncNullThrows(),
|
|
testFromSyncUndefinedThrows(),
|
|
testFromSyncTopLevelToStreamable(),
|
|
testFromSyncTopLevelProtocolOverIterator(),
|
|
testFromSyncIgnoresAsyncStreamable(),
|
|
testFromSyncRejectsAsyncIterable(),
|
|
testFromSyncRejectsPromise(),
|
|
testFromSyncDataView(),
|
|
]).then(common.mustCall());
|