Skip to content

Commit 2b1b0d8

Browse files
committed
stream: optimize forEach(), some(), every() and find() functions on Readable
Signed-off-by: Luciano Leggieri <230980@gmail.com> Assisted-by: Sol 5.6
1 parent f39899d commit 2b1b0d8

3 files changed

Lines changed: 335 additions & 18 deletions

File tree

‎lib/internal/streams/operators.js‎

Lines changed: 223 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
ArrayPrototypePush,
55
Boolean,
6+
FunctionPrototypeCall,
67
MathFloor,
78
Number,
89
NumberIsNaN,
@@ -12,6 +13,7 @@ const {
1213
PromisePrototypeThen,
1314
PromiseReject,
1415
PromiseResolve,
16+
PromiseWithResolvers,
1517
ReflectApply,
1618
Symbol,
1719
} = primordials;
@@ -20,6 +22,7 @@ const { AbortController, AbortSignal } = require('internal/abort_controller');
2022

2123
const {
2224
AbortError,
25+
aggregateTwoErrors,
2326
codes: {
2427
ERR_ILLEGAL_CONSTRUCTOR,
2528
ERR_MISSING_ARGS,
@@ -31,15 +34,45 @@ const {
3134
validateInteger,
3235
validateObject,
3336
validateFunction,
37+
validateBoolean,
3438
} = require('internal/validators');
3539
const { kWeakHandler, kResistStopPropagation } = require('internal/event_target');
3640
const destroyImpl = require('internal/streams/destroy');
37-
const { finished } = require('internal/streams/end-of-stream');
41+
const { eos, finished } = require('internal/streams/end-of-stream');
3842
const Stream = require('stream');
3943

4044
const kEmpty = Symbol('kEmpty');
4145
const kEof = Symbol('kEof');
4246

47+
const {
48+
isPromise,
49+
} = require('internal/util/types');
50+
51+
52+
// Return native promises unchanged and normalize custom thenables exactly once.
53+
// Capturing `then` avoids repeated getter access, while calling it with `value`
54+
// as the receiver preserves thenables that depend on their `this` value.
55+
function getThenablePromise(value) {
56+
if (isPromise(value)) {
57+
return value;
58+
}
59+
60+
const valueType = typeof value;
61+
if ((valueType === 'object' && value !== null) || valueType === 'function') {
62+
const then = value.then;
63+
if (typeof then === 'function') {
64+
return PromiseResolve({
65+
__proto__: null,
66+
then(resolve, reject) {
67+
FunctionPrototypeCall(then, value, resolve, reject);
68+
},
69+
});
70+
}
71+
}
72+
73+
return undefined;
74+
}
75+
4376
function map(fn, options) {
4477
validateFunction(fn, 'fn');
4578
if (options != null) {
@@ -188,36 +221,208 @@ function map(fn, options) {
188221
}.call(this);
189222
}
190223

191-
async function some(fn, options = undefined) {
192-
for await (const unused of filter.call(this, fn, options)) {
193-
return true;
224+
function nowOrLater(fn, fn2, args) {
225+
const value = fn(...args);
226+
const promise = getThenablePromise(value);
227+
if (promise !== undefined) {
228+
return PromisePrototypeThen(promise, fn2);
194229
}
195-
return false;
230+
return fn2(value);
231+
}
232+
233+
async function some(fn, options = undefined) {
234+
validateFunction(fn, 'fn');
235+
const someFn = (...args) => {
236+
return nowOrLater(fn, Boolean, args);
237+
};
238+
return (await find.call(this, someFn, options)) !== undefined;
196239
}
197240

198241
async function every(fn, options = undefined) {
199242
validateFunction(fn, 'fn');
243+
const everyFn = (...args) => {
244+
return nowOrLater(fn, (value) => !value, args);
245+
};
200246
// https://en.wikipedia.org/wiki/De_Morgan%27s_laws
201-
return !(await some.call(this, async (...args) => {
202-
return !(await fn(...args));
203-
}, options));
247+
return !(await find.call(this, everyFn, options));
204248
}
205249

206-
async function find(fn, options) {
207-
for await (const result of filter.call(this, fn, options)) {
208-
return result;
250+
function find(fn, options) {
251+
validateFunction(fn, 'fn');
252+
253+
if (options != null) {
254+
validateObject(options, 'options');
209255
}
210-
return undefined;
256+
const signal = options?.signal;
257+
if (signal != null) {
258+
validateAbortSignal(signal, 'options.signal');
259+
}
260+
261+
const concurrency = MathFloor(options?.concurrency ?? 1);
262+
validateInteger(concurrency, 'options.concurrency', 1);
263+
264+
const destroyOnReturn = options?.destroyOnReturn ?? true;
265+
validateBoolean(destroyOnReturn, 'options.destroyOnReturn');
266+
267+
const ac = new AbortController();
268+
const predicateSignal = AbortSignal.any([ac.signal, signal].filter(Boolean));
269+
const predicateOptions = { signal: predicateSignal };
270+
271+
// Concurrent predicates can settle out of order. Stop reading after any
272+
// match, but keep the lowest index after all active predicates settle.
273+
const stream = this;
274+
const { promise, resolve } = PromiseWithResolvers();
275+
let match;
276+
let error;
277+
let activeEvaluations = 0;
278+
let nextIndex = 0;
279+
let ended = false;
280+
let settled = false;
281+
let draining = false;
282+
283+
function settle() {
284+
if (!settled) {
285+
settled = true;
286+
resolve();
287+
}
288+
}
289+
290+
function fail(err) {
291+
if (settled) {
292+
return;
293+
}
294+
error = aggregateTwoErrors(error, err);
295+
destroyImpl.destroyer(stream, error);
296+
settle();
297+
}
298+
299+
function maybeSettle() {
300+
if (activeEvaluations === 0 && (match !== undefined || ended)) {
301+
settle();
302+
}
303+
}
304+
305+
function evaluationFinished(matches, chunk, index) {
306+
if (matches && (match === undefined || index < match.index)) {
307+
match = { index, value: chunk };
308+
}
309+
activeEvaluations--;
310+
maybeSettle();
311+
312+
if (!settled && match === undefined && !draining) {
313+
onReadable();
314+
}
315+
}
316+
317+
function evaluationRejected(err) {
318+
activeEvaluations--;
319+
fail(err);
320+
}
321+
322+
function evaluate(chunk, index) {
323+
activeEvaluations++;
324+
325+
let matches;
326+
try {
327+
matches = fn(chunk, predicateOptions);
328+
const matchesPromise = getThenablePromise(matches);
329+
if (matchesPromise !== undefined) {
330+
PromisePrototypeThen(
331+
matchesPromise,
332+
(result) => evaluationFinished(result, chunk, index),
333+
evaluationRejected,
334+
);
335+
return;
336+
}
337+
} catch (err) {
338+
evaluationRejected(err);
339+
return;
340+
}
341+
evaluationFinished(matches, chunk, index);
342+
}
343+
344+
function onReadable() {
345+
if (draining || settled || match !== undefined) {
346+
return;
347+
}
348+
349+
draining = true;
350+
try {
351+
while (
352+
!settled &&
353+
match === undefined &&
354+
activeEvaluations < concurrency
355+
) {
356+
if (signal?.aborted) {
357+
fail(new AbortError(undefined, { cause: signal.reason }));
358+
return;
359+
}
360+
361+
const chunk = stream.destroyed ? null : stream.read();
362+
if (chunk === null) {
363+
return;
364+
}
365+
evaluate(chunk, nextIndex++);
366+
}
367+
} catch (err) {
368+
fail(err);
369+
} finally {
370+
draining = false;
371+
}
372+
}
373+
374+
function onAbort() {
375+
fail(new AbortError(undefined, { cause: signal.reason }));
376+
}
377+
378+
stream.on('readable', onReadable);
379+
380+
const cleanup = eos(stream, { writable: false }, (err) => {
381+
if (settled) {
382+
return;
383+
}
384+
if (err) {
385+
fail(err);
386+
return;
387+
}
388+
ended = true;
389+
maybeSettle();
390+
});
391+
392+
if (signal != null) {
393+
signal.addEventListener('abort', onAbort, { once: true });
394+
if (signal.aborted) {
395+
onAbort();
396+
}
397+
}
398+
399+
return PromisePrototypeThen(promise, () => {
400+
stream.off('readable', onReadable);
401+
signal?.removeEventListener('abort', onAbort);
402+
ac.abort();
403+
404+
if (
405+
(error || destroyOnReturn !== false) &&
406+
(error === undefined || stream._readableState.autoDestroy)
407+
) {
408+
destroyImpl.destroyer(stream, error);
409+
} else {
410+
cleanup();
411+
}
412+
413+
if (error) {
414+
return PromiseReject(error);
415+
}
416+
return match?.value;
417+
});
211418
}
212419

213420
async function forEach(fn, options) {
214421
validateFunction(fn, 'fn');
215-
async function forEachFn(value, options) {
216-
await fn(value, options);
217-
return kEmpty;
218-
}
219-
// eslint-disable-next-line no-unused-vars
220-
for await (const unused of map.call(this, forEachFn, options));
422+
const forEachFn = (...args) => {
423+
return nowOrLater(fn, () => false, args);
424+
};
425+
await find.call(this, forEachFn, options);
221426
}
222427

223428
function filter(fn, options) {

‎test/parallel/test-stream-forEach.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ const { once } = require('events');
4343
})().then(common.mustCall());
4444
}
4545

46+
{
47+
// forEach awaits thenables returned by the callback.
48+
const visited = [];
49+
const receivers = [];
50+
const thenables = [];
51+
(async () => {
52+
await Readable.from([1, 2]).forEach((value) => {
53+
const thenable = {
54+
then(resolve) {
55+
receivers.push(this);
56+
setImmediate(() => {
57+
visited.push(value);
58+
resolve();
59+
});
60+
},
61+
};
62+
thenables.push(thenable);
63+
return thenable;
64+
});
65+
assert.deepStrictEqual(visited, [1, 2]);
66+
assert.deepStrictEqual(receivers, thenables);
67+
})().then(common.mustCall());
68+
}
69+
4670
{
4771
// forEach works on an infinite stream
4872
const ac = new AbortController();

0 commit comments

Comments
 (0)