Skip to content

Commit 77c1868

Browse files
benjamingrdanielleadams
authored andcommitted
stream: add filter method to readable
This continues the work in #40815 to make streams compatible with upcoming ECMAScript language features. It adds an experimental `filter` api to streams and tests/docs for it. See https://github.com/tc39/proposal-iterator-helpers/ Co-Authored-By: Robert Nagy <ronagy@icloud.com> PR-URL: #41354 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 7618b55 commit 77c1868

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

‎doc/api/stream.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,55 @@ for await (const result of dnsResults) {
17261726
}
17271727
```
17281728

1729+
### `readable.filter(fn[, options])`
1730+
1731+
<!-- YAML
1732+
added: REPLACEME
1733+
-->
1734+
1735+
> Stability: 1 - Experimental
1736+
1737+
*`fn` {Function|AsyncFunction} a function to filter items from stream.
1738+
*`data` {any} a chunk of data from the stream.
1739+
*`options` {Object}
1740+
*`signal` {AbortSignal} aborted if the stream is destroyed allowing to
1741+
abort the `fn` call early.
1742+
*`options` {Object}
1743+
*`concurrency` {number} the maximal concurrent invocation of `fn` to call
1744+
on the stream at once. **Default:**`1`.
1745+
*`signal` {AbortSignal} allows destroying the stream if the signal is
1746+
aborted.
1747+
* Returns: {Readable} a stream filtered with the predicate `fn`.
1748+
1749+
This method allows filtering the stream. For each item in the stream the `fn`
1750+
function will be called and if it returns a truthy value, the item will be
1751+
passed to the result stream. If the `fn` function returns a promise - that
1752+
promise will be `await`ed.
1753+
1754+
```mjs
1755+
import { Readable } from'stream';
1756+
import { Resolver } from'dns/promises';
1757+
1758+
// With a synchronous predicate.
1759+
forawait (constitemofReadable.from([1, 2, 3, 4]).filter((x) => x >2)) {
1760+
console.log(item); // 3, 4
1761+
}
1762+
// With an asynchronous predicate, making at most 2 queries at a time.
1763+
constresolver=newResolver();
1764+
constdnsResults=awaitReadable.from([
1765+
'nodejs.org',
1766+
'openjsf.org',
1767+
'www.linuxfoundation.org',
1768+
]).filter(async (domain) => {
1769+
const { address } =awaitresolver.resolve4(domain, { ttl:true });
1770+
returnaddress.ttl>60;
1771+
}, { concurrency:2 });
1772+
forawait (constresultofdnsResults) {
1773+
// Logs domains with more than 60 seconds on the resolved dns record.
1774+
console.log(result);
1775+
}
1776+
```
1777+
17291778
### Duplex and transform streams
17301779

17311780
#### Class: `stream.Duplex`

‎lib/internal/streams/operators.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ async function * map(fn, options) {
147147
}
148148
}
149149

150+
asyncfunction*filter(fn,options){
151+
if(typeoffn!=='function'){
152+
throw(newERR_INVALID_ARG_TYPE(
153+
'fn',['Function','AsyncFunction'],this));
154+
}
155+
asyncfunctionfilterFn(value,options){
156+
if(awaitfn(value,options)){
157+
returnvalue;
158+
}
159+
returnkEmpty;
160+
}
161+
yield*this.map(filterFn,options);
162+
}
150163
module.exports={
151164
map,
165+
filter
152166
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
const{
5+
Readable,
6+
}=require('stream');
7+
constassert=require('assert');
8+
const{ setTimeout }=require('timers/promises');
9+
10+
{
11+
// Filter works on synchronous streams with a synchronous predicate
12+
conststream=Readable.from([1,2,3,4,5]).filter((x)=>x<3);
13+
constresult=[1,2];
14+
(async()=>{
15+
forawait(constitemofstream){
16+
assert.strictEqual(item,result.shift());
17+
}
18+
})().then(common.mustCall());
19+
}
20+
21+
{
22+
// Filter works on synchronous streams with an asynchronous predicate
23+
conststream=Readable.from([1,2,3,4,5]).filter(async(x)=>{
24+
awaitPromise.resolve();
25+
returnx>3;
26+
});
27+
constresult=[4,5];
28+
(async()=>{
29+
forawait(constitemofstream){
30+
assert.strictEqual(item,result.shift());
31+
}
32+
})().then(common.mustCall());
33+
}
34+
35+
{
36+
// Map works on asynchronous streams with a asynchronous mapper
37+
conststream=Readable.from([1,2,3,4,5]).map(async(x)=>{
38+
awaitPromise.resolve();
39+
returnx+x;
40+
}).filter((x)=>x>5);
41+
constresult=[6,8,10];
42+
(async()=>{
43+
forawait(constitemofstream){
44+
assert.strictEqual(item,result.shift());
45+
}
46+
})().then(common.mustCall());
47+
}
48+
49+
{
50+
// Concurrency + AbortSignal
51+
constac=newAbortController();
52+
letcalls=0;
53+
conststream=Readable.from([1,2,3,4]).filter(async(_,{ signal })=>{
54+
calls++;
55+
awaitsetTimeout(100,{ signal });
56+
},{signal: ac.signal,concurrency: 2});
57+
// pump
58+
assert.rejects(async()=>{
59+
forawait(constitemofstream){
60+
// nope
61+
console.log(item);
62+
}
63+
},{
64+
name: 'AbortError',
65+
}).then(common.mustCall());
66+
67+
setImmediate(()=>{
68+
ac.abort();
69+
assert.strictEqual(calls,2);
70+
});
71+
}
72+
73+
{
74+
// Concurrency result order
75+
conststream=Readable.from([1,2]).filter(async(item,{ signal })=>{
76+
awaitsetTimeout(10-item,{ signal });
77+
returntrue;
78+
},{concurrency: 2});
79+
80+
(async()=>{
81+
constexpected=[1,2];
82+
forawait(constitemofstream){
83+
assert.strictEqual(item,expected.shift());
84+
}
85+
})().then(common.mustCall());
86+
}
87+
88+
{
89+
// Error cases
90+
assert.rejects(async()=>{
91+
// eslint-disable-next-line no-unused-vars
92+
forawait(constunusedofReadable.from([1]).filter(1));
93+
},/ERR_INVALID_ARG_TYPE/).then(common.mustCall());
94+
assert.rejects(async()=>{
95+
// eslint-disable-next-line no-unused-vars
96+
forawait(const_ofReadable.from([1]).filter((x)=>x,{
97+
concurrency: 'Foo'
98+
}));
99+
},/ERR_OUT_OF_RANGE/).then(common.mustCall());
100+
assert.rejects(async()=>{
101+
// eslint-disable-next-line no-unused-vars
102+
forawait(const_ofReadable.from([1]).filter((x)=>x,1));
103+
},/ERR_INVALID_ARG_TYPE/).then(common.mustCall());
104+
}
105+
{
106+
// Test result is a Readable
107+
conststream=Readable.from([1,2,3,4,5]).filter((x)=>true);
108+
assert.strictEqual(stream.readable,true);
109+
}

0 commit comments

Comments
 (0)