Skip to content

Commit ddb5152

Browse files
guybedfordBethGriggs
authored andcommitted
stream: implement Readable.from async iterator utility
PR-URL: #27660 Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent 333963e commit ddb5152

File tree

4 files changed

+314
-3
lines changed

4 files changed

+314
-3
lines changed

doc/api/stream.md

+111-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ There are four fundamental stream types within Node.js:
4646
* [`Transform`][] - `Duplex` streams that can modify or transform the data as it
4747
is written and read (for example, [`zlib.createDeflate()`][]).
4848

49-
Additionally, this module includes the utility functions [pipeline][] and
50-
[finished][].
49+
Additionally, this module includes the utility functions [pipeline][],
50+
[finished][] and [Readable.from][].
5151

5252
### Object Mode
5353

@@ -1445,6 +1445,31 @@ async function run() {
14451445
run().catch(console.error);
14461446
```
14471447

1448+
### Readable.from(iterable, [options])
1449+
1450+
* `iterable` {Iterable} Object implementing the `Symbol.asyncIterator` or
1451+
`Symbol.iterator` iterable protocol.
1452+
* `options` {Object} Options provided to `new stream.Readable([options])`.
1453+
By default, `Readable.from()` will set `options.objectMode` to `true`, unless
1454+
this is explicitly opted out by setting `options.objectMode` to `false`.
1455+
1456+
A utility method for creating Readable Streams out of iterators.
1457+
1458+
```js
1459+
const { Readable } = require('stream');
1460+
1461+
async function * generate() {
1462+
yield 'hello';
1463+
yield 'streams';
1464+
}
1465+
1466+
const readable = Readable.from(generate());
1467+
1468+
readable.on('data', (chunk) => {
1469+
console.log(chunk);
1470+
});
1471+
```
1472+
14481473
## API for Stream Implementers
14491474

14501475
<!--type=misc-->
@@ -2368,6 +2393,89 @@ primarily for examples and testing, but there are some use cases where
23682393

23692394
<!--type=misc-->
23702395

2396+
### Streams Compatibility with Async Generators and Async Iterators
2397+
2398+
With the support of async generators and iterators in JavaScript, async
2399+
generators are effectively a first-class language-level stream construct at
2400+
this point.
2401+
2402+
Some common interop cases of using Node.js streams with async generators
2403+
and async iterators are provided below.
2404+
2405+
#### Consuming Readable Streams with Async Iterators
2406+
2407+
```js
2408+
(async function() {
2409+
for await (const chunk of readable) {
2410+
console.log(chunk);
2411+
}
2412+
})();
2413+
```
2414+
2415+
#### Creating Readable Streams with Async Generators
2416+
2417+
We can construct a Node.js Readable Stream from an asynchronous generator
2418+
using the `Readable.from` utility method:
2419+
2420+
```js
2421+
const { Readable } = require('stream');
2422+
2423+
async function * generate() {
2424+
yield 'a';
2425+
yield 'b';
2426+
yield 'c';
2427+
}
2428+
2429+
const readable = Readable.from(generate());
2430+
2431+
readable.on('data', (chunk) => {
2432+
console.log(chunk);
2433+
});
2434+
```
2435+
2436+
#### Piping to Writable Streams from Async Iterators
2437+
2438+
In the scenario of writing to a writeable stream from an async iterator,
2439+
it is important to ensure the correct handling of backpressure and errors.
2440+
2441+
```js
2442+
const { once } = require('events');
2443+
2444+
const writeable = fs.createWriteStream('./file');
2445+
2446+
(async function() {
2447+
for await (const chunk of iterator) {
2448+
// Handle backpressure on write
2449+
if (!writeable.write(value))
2450+
await once(writeable, 'drain');
2451+
}
2452+
writeable.end();
2453+
// Ensure completion without errors
2454+
await once(writeable, 'finish');
2455+
})();
2456+
```
2457+
2458+
In the above, errors on the write stream would be caught and thrown by the two
2459+
`once` listeners, since `once` will also handle `'error'` events.
2460+
2461+
Alternatively the readable stream could be wrapped with `Readable.from` and
2462+
then piped via `.pipe`:
2463+
2464+
```js
2465+
const { once } = require('events');
2466+
2467+
const writeable = fs.createWriteStream('./file');
2468+
2469+
(async function() {
2470+
const readable = Readable.from(iterator);
2471+
readable.pipe(writeable);
2472+
// Ensure completion without errors
2473+
await once(writeable, 'finish');
2474+
})();
2475+
```
2476+
2477+
<!--type=misc-->
2478+
23712479
### Compatibility with Older Node.js Versions
23722480

23732481
<!--type=misc-->
@@ -2504,6 +2612,7 @@ contain multi-byte characters.
25042612
[Compatibility]: #stream_compatibility_with_older_node_js_versions
25052613
[HTTP requests, on the client]: http.html#http_class_http_clientrequest
25062614
[HTTP responses, on the server]: http.html#http_class_http_serverresponse
2615+
[Readable.from]: #readable.from
25072616
[TCP sockets]: net.html#net_class_net_socket
25082617
[child process stdin]: child_process.html#child_process_subprocess_stdin
25092618
[child process stdout and stderr]: child_process.html#child_process_subprocess_stdout

lib/_stream_readable.js

+39
Original file line numberDiff line numberDiff line change
@@ -1154,3 +1154,42 @@ function endReadableNT(state, stream) {
11541154
}
11551155
}
11561156
}
1157+
1158+
Readable.from = function(iterable, opts) {
1159+
let iterator;
1160+
if (iterable && iterable[Symbol.asyncIterator])
1161+
iterator = iterable[Symbol.asyncIterator]();
1162+
else if (iterable && iterable[Symbol.iterator])
1163+
iterator = iterable[Symbol.iterator]();
1164+
else
1165+
throw new ERR_INVALID_ARG_TYPE('iterable', ['Iterable'], iterable);
1166+
1167+
const readable = new Readable({
1168+
objectMode: true,
1169+
...opts
1170+
});
1171+
// Reading boolean to protect against _read
1172+
// being called before last iteration completion.
1173+
let reading = false;
1174+
readable._read = function() {
1175+
if (!reading) {
1176+
reading = true;
1177+
next();
1178+
}
1179+
};
1180+
async function next() {
1181+
try {
1182+
const { value, done } = await iterator.next();
1183+
if (done) {
1184+
readable.push(null);
1185+
} else if (readable.push(await value)) {
1186+
next();
1187+
} else {
1188+
reading = false;
1189+
}
1190+
} catch (err) {
1191+
readable.destroy(err);
1192+
}
1193+
}
1194+
return readable;
1195+
};

test/parallel/test-events-once.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ Promise.all([
9090
catchesErrors(),
9191
stopListeningAfterCatchingError(),
9292
onceError()
93-
]);
93+
]).then(common.mustCall());

test/parallel/test-readable-from.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
'use strict';
2+
3+
const { mustCall } = require('../common');
4+
const { once } = require('events');
5+
const { Readable } = require('stream');
6+
const { strictEqual } = require('assert');
7+
8+
async function toReadableBasicSupport() {
9+
async function * generate() {
10+
yield 'a';
11+
yield 'b';
12+
yield 'c';
13+
}
14+
15+
const stream = Readable.from(generate());
16+
17+
const expected = ['a', 'b', 'c'];
18+
19+
for await (const chunk of stream) {
20+
strictEqual(chunk, expected.shift());
21+
}
22+
}
23+
24+
async function toReadableSyncIterator() {
25+
function * generate() {
26+
yield 'a';
27+
yield 'b';
28+
yield 'c';
29+
}
30+
31+
const stream = Readable.from(generate());
32+
33+
const expected = ['a', 'b', 'c'];
34+
35+
for await (const chunk of stream) {
36+
strictEqual(chunk, expected.shift());
37+
}
38+
}
39+
40+
async function toReadablePromises() {
41+
const promises = [
42+
Promise.resolve('a'),
43+
Promise.resolve('b'),
44+
Promise.resolve('c')
45+
];
46+
47+
const stream = Readable.from(promises);
48+
49+
const expected = ['a', 'b', 'c'];
50+
51+
for await (const chunk of stream) {
52+
strictEqual(chunk, expected.shift());
53+
}
54+
}
55+
56+
async function toReadableString() {
57+
const stream = Readable.from('abc');
58+
59+
const expected = ['a', 'b', 'c'];
60+
61+
for await (const chunk of stream) {
62+
strictEqual(chunk, expected.shift());
63+
}
64+
}
65+
66+
async function toReadableOnData() {
67+
async function * generate() {
68+
yield 'a';
69+
yield 'b';
70+
yield 'c';
71+
}
72+
73+
const stream = Readable.from(generate());
74+
75+
let iterations = 0;
76+
const expected = ['a', 'b', 'c'];
77+
78+
stream.on('data', (chunk) => {
79+
iterations++;
80+
strictEqual(chunk, expected.shift());
81+
});
82+
83+
await once(stream, 'end');
84+
85+
strictEqual(iterations, 3);
86+
}
87+
88+
async function toReadableOnDataNonObject() {
89+
async function * generate() {
90+
yield 'a';
91+
yield 'b';
92+
yield 'c';
93+
}
94+
95+
const stream = Readable.from(generate(), { objectMode: false });
96+
97+
let iterations = 0;
98+
const expected = ['a', 'b', 'c'];
99+
100+
stream.on('data', (chunk) => {
101+
iterations++;
102+
strictEqual(chunk instanceof Buffer, true);
103+
strictEqual(chunk.toString(), expected.shift());
104+
});
105+
106+
await once(stream, 'end');
107+
108+
strictEqual(iterations, 3);
109+
}
110+
111+
async function destroysTheStreamWhenThrowing() {
112+
async function * generate() {
113+
throw new Error('kaboom');
114+
}
115+
116+
const stream = Readable.from(generate());
117+
118+
stream.read();
119+
120+
try {
121+
await once(stream, 'error');
122+
} catch (err) {
123+
strictEqual(err.message, 'kaboom');
124+
strictEqual(stream.destroyed, true);
125+
}
126+
}
127+
128+
async function asTransformStream() {
129+
async function * generate(stream) {
130+
for await (const chunk of stream) {
131+
yield chunk.toUpperCase();
132+
}
133+
}
134+
135+
const source = new Readable({
136+
objectMode: true,
137+
read() {
138+
this.push('a');
139+
this.push('b');
140+
this.push('c');
141+
this.push(null);
142+
}
143+
});
144+
145+
const stream = Readable.from(generate(source));
146+
147+
const expected = ['A', 'B', 'C'];
148+
149+
for await (const chunk of stream) {
150+
strictEqual(chunk, expected.shift());
151+
}
152+
}
153+
154+
Promise.all([
155+
toReadableBasicSupport(),
156+
toReadableSyncIterator(),
157+
toReadablePromises(),
158+
toReadableString(),
159+
toReadableOnData(),
160+
toReadableOnDataNonObject(),
161+
destroysTheStreamWhenThrowing(),
162+
asTransformStream()
163+
]).then(mustCall());

0 commit comments

Comments
 (0)