Skip to content

Commit f159202

Browse files
benjamingrronag
andcommitted
stream: support some and every
This continues on the iterator-helpers work by adding `.some` and `.every` to readable streams. Co-Authored-By: Robert Nagy <[email protected]>
1 parent 5a407d6 commit f159202

File tree

3 files changed

+233
-1
lines changed

3 files changed

+233
-1
lines changed

doc/api/stream.md

+97-1
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,7 @@ import { Resolver } from 'dns/promises';
19181918
await Readable.from([1, 2, 3, 4]).toArray(); // [1, 2, 3, 4]
19191919

19201920
// Make dns queries concurrently using .map and collect
1921-
// the results into an aray using toArray
1921+
// the results into an array using toArray
19221922
const dnsResults = await Readable.from([
19231923
'nodejs.org',
19241924
'openjsf.org',
@@ -1929,6 +1929,102 @@ const dnsResults = await Readable.from([
19291929
}, { concurrency: 2 }).toArray();
19301930
```
19311931

1932+
### `readable.some(fn[, options])`
1933+
1934+
<!-- YAML
1935+
added: REPLACEME
1936+
-->
1937+
1938+
> Stability: 1 - Experimental
1939+
1940+
* `fn` {Function|AsyncFunction} a function to call on each item of the stream.
1941+
* `data` {any} a chunk of data from the stream.
1942+
* `options` {Object}
1943+
* `signal` {AbortSignal} aborted if the stream is destroyed allowing to
1944+
abort the `fn` call early.
1945+
* `options` {Object}
1946+
* `concurrency` {number} the maximum concurrent invocation of `fn` to call
1947+
on the stream at once. **Default:** `1`.
1948+
* `signal` {AbortSignal} allows destroying the stream if the signal is
1949+
aborted.
1950+
* Returns: {Promise} a promise evaluating to `true` if `fn` returned a truthy
1951+
value for some of the chunks.
1952+
1953+
This method is similar to `Array.prototype.some` and calls `fn` on each chunk
1954+
in the stream until one item returns true (or any truthy value). Once an `fn`
1955+
call on a chunk returns a truthy value the stream is destroyed and the promise
1956+
is fulfilled with `true`. If none of the `fn` calls on the chunks return a
1957+
truthy value the promise is fulfilled with `false`.
1958+
1959+
```mjs
1960+
import { Readable } from 'stream';
1961+
import { stat } from 'fs/promises';
1962+
1963+
// With a synchronous predicate.
1964+
await Readable.from([1, 2, 3, 4]).some((x) => x > 2); // true
1965+
await Readable.from([1, 2, 3, 4]).some((x) => x < 0); // false
1966+
1967+
// With an asynchronous predicate, making at most 2 file checks at a time.
1968+
const anyBigFile = await Readable.from([
1969+
'file1',
1970+
'file2',
1971+
'file3',
1972+
]).some(async (fileName) => {
1973+
const stats = await stat(fileName);
1974+
return stat.size > 1024 * 1024;
1975+
}, { concurrency: 2 });
1976+
console.log(anyBigFile); // `true` if any file in the list is bigger than 1MB
1977+
console.log('done'); // Stream has finished
1978+
```
1979+
1980+
### `readable.every(fn[, options])`
1981+
1982+
<!-- YAML
1983+
added: REPLACEME
1984+
-->
1985+
1986+
> Stability: 1 - Experimental
1987+
1988+
* `fn` {Function|AsyncFunction} a function to call on each item of the stream.
1989+
* `data` {any} a chunk of data from the stream.
1990+
* `options` {Object}
1991+
* `signal` {AbortSignal} aborted if the stream is destroyed allowing to
1992+
abort the `fn` call early.
1993+
* `options` {Object}
1994+
* `concurrency` {number} the maximum concurrent invocation of `fn` to call
1995+
on the stream at once. **Default:** `1`.
1996+
* `signal` {AbortSignal} allows destroying the stream if the signal is
1997+
aborted.
1998+
* Returns: {Promise} a promise evaluating to `true` if `fn` returned a truthy
1999+
value for all of the chunks.
2000+
2001+
This method is similar to `Array.prototype.every` and calls `fn` on each chunk
2002+
in the stream to check if they all return a truthy value for `fn`. Once an `fn`
2003+
call on a chunk returns a falsy value the stream is destroyed and the promise
2004+
is fulfilled with `false`. If all of the `fn` calls on the chunks return a
2005+
truthy value the promise is fulfilled with `true`.
2006+
2007+
```mjs
2008+
import { Readable } from 'stream';
2009+
import { stat } from 'fs/promises';
2010+
2011+
// With a synchronous predicate.
2012+
await Readable.from([1, 2, 3, 4]).every((x) => x > 2); // false
2013+
await Readable.from([1, 2, 3, 4]).every((x) => x > 0); // true
2014+
2015+
// With an asynchronous predicate, making at most 2 file checks at a time.
2016+
const allBigFiles = await Readable.from([
2017+
'file1',
2018+
'file2',
2019+
'file3',
2020+
]).every(async (fileName) => {
2021+
const stats = await stat(fileName);
2022+
return stat.size > 1024 * 1024;
2023+
}, { concurrency: 2 });
2024+
console.log(anyBigFile); // `true` if all files in the list are bigger than 1MiB
2025+
console.log('done'); // Stream has finished
2026+
```
2027+
19322028
### Duplex and transform streams
19332029

19342030
#### Class: `stream.Duplex`

lib/internal/streams/operators.js

+37
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
AbortError,
1111
} = require('internal/errors');
1212
const { validateInteger } = require('internal/validators');
13+
const { kWeakHandler } = require('internal/event_target');
1314

1415
const {
1516
ArrayPrototypePush,
@@ -150,6 +151,40 @@ async function * map(fn, options) {
150151
}
151152
}
152153

154+
async function some(fn, options) {
155+
// https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.some
156+
// Note that some does short circuit but also closes the iterator if it does
157+
const ac = new AbortController();
158+
if (options?.signal) {
159+
if (options.signal.aborted) {
160+
ac.abort();
161+
}
162+
options.signal.addEventListener('abort', () => ac.abort(), {
163+
[kWeakHandler]: this,
164+
once: true,
165+
});
166+
}
167+
const mapped = this.map(fn, { ...options, signal: ac.signal });
168+
for await (const result of mapped) {
169+
if (result) {
170+
ac.abort();
171+
return true;
172+
}
173+
}
174+
return false;
175+
}
176+
177+
async function every(fn, options) {
178+
if (typeof fn !== 'function') {
179+
throw new ERR_INVALID_ARG_TYPE(
180+
'fn', ['Function', 'AsyncFunction'], fn);
181+
}
182+
// https://en.wikipedia.org/wiki/De_Morgan%27s_laws
183+
return !(await some.call(this, async (x) => {
184+
return !(await fn(x));
185+
}, options));
186+
}
187+
153188
async function forEach(fn, options) {
154189
if (typeof fn !== 'function') {
155190
throw new ERR_INVALID_ARG_TYPE(
@@ -196,6 +231,8 @@ module.exports.streamReturningOperators = {
196231
};
197232

198233
module.exports.promiseReturningOperators = {
234+
every,
199235
forEach,
200236
toArray,
237+
some,
201238
};
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const {
5+
Readable,
6+
} = require('stream');
7+
const assert = require('assert');
8+
9+
function oneTo5() {
10+
return Readable.from([1, 2, 3, 4, 5]);
11+
}
12+
13+
function oneTo5Async() {
14+
return oneTo5().map(async (x) => {
15+
await Promise.resolve();
16+
return x;
17+
});
18+
}
19+
{
20+
// Some and every work with a synchronous stream and predicate
21+
(async () => {
22+
assert.strictEqual(await oneTo5().some((x) => x > 3), true);
23+
assert.strictEqual(await oneTo5().every((x) => x > 3), false);
24+
assert.strictEqual(await oneTo5().some((x) => x > 6), false);
25+
assert.strictEqual(await oneTo5().every((x) => x < 6), true);
26+
assert.strictEqual(await Readable.from([]).some((x) => true), false);
27+
assert.strictEqual(await Readable.from([]).every((x) => true), true);
28+
})().then(common.mustCall());
29+
}
30+
31+
{
32+
// Some and every work with an asynchronous stream and synchronous predicate
33+
(async () => {
34+
assert.strictEqual(await oneTo5Async().some((x) => x > 3), true);
35+
assert.strictEqual(await oneTo5Async().every((x) => x > 3), false);
36+
assert.strictEqual(await oneTo5Async().some((x) => x > 6), false);
37+
assert.strictEqual(await oneTo5Async().every((x) => x < 6), true);
38+
})().then(common.mustCall());
39+
}
40+
41+
{
42+
// Some and every work on asynchronous streams with an asynchronous predicate
43+
(async () => {
44+
assert.strictEqual(await oneTo5().some(async (x) => x > 3), true);
45+
assert.strictEqual(await oneTo5().every(async (x) => x > 3), false);
46+
assert.strictEqual(await oneTo5().some(async (x) => x > 6), false);
47+
assert.strictEqual(await oneTo5().every(async (x) => x < 6), true);
48+
})().then(common.mustCall());
49+
}
50+
51+
{
52+
// Some and every short circuit
53+
(async () => {
54+
await oneTo5().some(common.mustCall((x) => x > 2, 3));
55+
await oneTo5().every(common.mustCall((x) => x < 3, 3));
56+
// When short circuit isn't possible the whole stream is iterated
57+
await oneTo5().some(common.mustCall((x) => x > 6, 5));
58+
// The stream is destroyed afterwards
59+
const stream = oneTo5();
60+
await stream.some(common.mustCall((x) => x > 2, 3));
61+
assert.strictEqual(stream.destroyed, true);
62+
})().then(common.mustCall());
63+
}
64+
65+
{
66+
// Support for AbortSignal
67+
const ac = new AbortController();
68+
assert.rejects(async () => {
69+
await Readable.from([1, 2, 3]).some(
70+
async (x) => new Promise(() => {}),
71+
{ signal: ac.signal });
72+
}, {
73+
name: 'AbortError',
74+
});
75+
ac.abort();
76+
}
77+
{
78+
// Support for pre-aborted AbortSignal
79+
const ac = new AbortController();
80+
ac.abort();
81+
assert.rejects(async () => {
82+
await Readable.from([1, 2, 3]).some(
83+
async (x) => new Promise(() => {}),
84+
{ signal: ac.signal });
85+
}, {
86+
name: 'AbortError',
87+
});
88+
}
89+
{
90+
// Error cases
91+
assert.rejects(async () => {
92+
await Readable.from([1]).every(1);
93+
}, /ERR_INVALID_ARG_TYPE/).then(common.mustCall());
94+
assert.rejects(async () => {
95+
await Readable.from([1]).every((x) => x, {
96+
concurrency: 'Foo'
97+
});
98+
}, /ERR_OUT_OF_RANGE/).then(common.mustCall());
99+
}

0 commit comments

Comments
 (0)