@naturalcycles/nodejs-lib 15.70.1 → 15.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/stream/index.d.ts +0 -1
- package/dist/stream/index.js +0 -1
- package/dist/stream/pipeline.d.ts +2 -5
- package/dist/stream/pipeline.js +3 -22
- package/dist/stream/transform/transformFilter.js +2 -2
- package/dist/stream/transform/transformMap.d.ts +13 -25
- package/dist/stream/transform/transformMap.js +134 -118
- package/dist/stream/transform/worker/transformMultiThreaded.js +59 -39
- package/package.json +1 -3
- package/src/stream/index.ts +0 -1
- package/src/stream/pipeline.ts +4 -35
- package/src/stream/transform/transformFilter.ts +2 -2
- package/src/stream/transform/transformMap.ts +168 -153
- package/src/stream/transform/worker/transformMultiThreaded.ts +57 -40
- package/src/stream/transform/worker/workerClassProxy.js +0 -4
- package/dist/stream/transform/transformMap2.d.ts +0 -66
- package/dist/stream/transform/transformMap2.js +0 -171
- package/src/stream/transform/transformMap2.ts +0 -283
package/dist/stream/index.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export * from './transform/transformFork.js';
|
|
|
16
16
|
export * from './transform/transformLimit.js';
|
|
17
17
|
export * from './transform/transformLogProgress.js';
|
|
18
18
|
export * from './transform/transformMap.js';
|
|
19
|
-
export * from './transform/transformMap2.js';
|
|
20
19
|
export * from './transform/transformMapSimple.js';
|
|
21
20
|
export * from './transform/transformMapSync.js';
|
|
22
21
|
export * from './transform/transformNoOp.js';
|
package/dist/stream/index.js
CHANGED
|
@@ -16,7 +16,6 @@ export * from './transform/transformFork.js';
|
|
|
16
16
|
export * from './transform/transformLimit.js';
|
|
17
17
|
export * from './transform/transformLogProgress.js';
|
|
18
18
|
export * from './transform/transformMap.js';
|
|
19
|
-
export * from './transform/transformMap2.js';
|
|
20
19
|
export * from './transform/transformMapSimple.js';
|
|
21
20
|
export * from './transform/transformMapSync.js';
|
|
22
21
|
export * from './transform/transformNoOp.js';
|
|
@@ -5,7 +5,6 @@ import { type AbortableAsyncMapper, type AsyncIndexedMapper, type AsyncPredicate
|
|
|
5
5
|
import type { ReadableTyped, TransformOptions, TransformTyped, WritableTyped } from './stream.model.js';
|
|
6
6
|
import { type TransformLogProgressOptions } from './transform/transformLogProgress.js';
|
|
7
7
|
import { type TransformMapOptions } from './transform/transformMap.js';
|
|
8
|
-
import { type TransformMap2Options } from './transform/transformMap2.js';
|
|
9
8
|
import { type TransformMapSimpleOptions } from './transform/transformMapSimple.js';
|
|
10
9
|
import { type TransformMapSyncOptions } from './transform/transformMapSync.js';
|
|
11
10
|
import { type TransformOffsetOptions } from './transform/transformOffset.js';
|
|
@@ -62,8 +61,7 @@ export declare class Pipeline<T = unknown> {
|
|
|
62
61
|
flatten<TO>(this: Pipeline<readonly TO[]>): Pipeline<TO>;
|
|
63
62
|
flattenIfNeeded(): Pipeline<T extends readonly (infer TO)[] ? TO : T>;
|
|
64
63
|
logProgress(opt?: TransformLogProgressOptions): this;
|
|
65
|
-
|
|
66
|
-
map<TO>(mapper: AbortableAsyncMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMap2Options<T, TO>): Pipeline<TO>;
|
|
64
|
+
map<TO>(mapper: AbortableAsyncMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMapOptions<T, TO>): Pipeline<TO>;
|
|
67
65
|
mapSync<TO>(mapper: IndexedMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMapSyncOptions): Pipeline<TO>;
|
|
68
66
|
mapSimple<TO>(mapper: IndexedMapper<T, TO>, opt?: TransformMapSimpleOptions): Pipeline<TO>;
|
|
69
67
|
filter(asyncPredicate: AsyncPredicate<T>, opt?: TransformMapOptions): this;
|
|
@@ -105,8 +103,7 @@ export declare class Pipeline<T = unknown> {
|
|
|
105
103
|
toFile(outputFilePath: string): Promise<void>;
|
|
106
104
|
toNDJsonFile(outputFilePath: string): Promise<void>;
|
|
107
105
|
to(destination: WritableTyped<T>): Promise<void>;
|
|
108
|
-
|
|
109
|
-
forEach(fn: AsyncIndexedMapper<T, void>, opt?: TransformMap2Options<T, void> & TransformLogProgressOptions<T>): Promise<void>;
|
|
106
|
+
forEach(fn: AsyncIndexedMapper<T, void>, opt?: TransformMapOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
|
|
110
107
|
forEachSync(fn: IndexedMapper<T, void>, opt?: TransformMapSyncOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
|
|
111
108
|
run(): Promise<void>;
|
|
112
109
|
}
|
package/dist/stream/pipeline.js
CHANGED
|
@@ -16,7 +16,6 @@ import { transformFork } from './transform/transformFork.js';
|
|
|
16
16
|
import { transformLimit } from './transform/transformLimit.js';
|
|
17
17
|
import { transformLogProgress, } from './transform/transformLogProgress.js';
|
|
18
18
|
import { transformMap } from './transform/transformMap.js';
|
|
19
|
-
import { transformMap2 } from './transform/transformMap2.js';
|
|
20
19
|
import { transformMapSimple, } from './transform/transformMapSimple.js';
|
|
21
20
|
import { transformMapSync } from './transform/transformMapSync.js';
|
|
22
21
|
import { transformOffset } from './transform/transformOffset.js';
|
|
@@ -128,15 +127,8 @@ export class Pipeline {
|
|
|
128
127
|
this.transforms.push(transformLogProgress(opt));
|
|
129
128
|
return this;
|
|
130
129
|
}
|
|
131
|
-
mapLegacy(mapper, opt) {
|
|
132
|
-
this.transforms.push(transformMap(mapper, {
|
|
133
|
-
...opt,
|
|
134
|
-
signal: this.abortableSignal,
|
|
135
|
-
}));
|
|
136
|
-
return this;
|
|
137
|
-
}
|
|
138
130
|
map(mapper, opt) {
|
|
139
|
-
this.transforms.push(
|
|
131
|
+
this.transforms.push(transformMap(mapper, {
|
|
140
132
|
...opt,
|
|
141
133
|
signal: this.abortableSignal,
|
|
142
134
|
}));
|
|
@@ -154,7 +146,7 @@ export class Pipeline {
|
|
|
154
146
|
return this;
|
|
155
147
|
}
|
|
156
148
|
filter(asyncPredicate, opt) {
|
|
157
|
-
this.transforms.push(
|
|
149
|
+
this.transforms.push(transformMap(v => v, {
|
|
158
150
|
asyncPredicate,
|
|
159
151
|
...opt,
|
|
160
152
|
signal: this.abortableSignal,
|
|
@@ -310,19 +302,8 @@ export class Pipeline {
|
|
|
310
302
|
this.destination = destination;
|
|
311
303
|
await this.run();
|
|
312
304
|
}
|
|
313
|
-
async forEachLegacy(fn, opt = {}) {
|
|
314
|
-
this.transforms.push(transformMap2(fn, {
|
|
315
|
-
predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
|
|
316
|
-
...opt,
|
|
317
|
-
signal: this.abortableSignal,
|
|
318
|
-
}));
|
|
319
|
-
if (opt.logEvery) {
|
|
320
|
-
this.transforms.push(transformLogProgress(opt));
|
|
321
|
-
}
|
|
322
|
-
await this.run();
|
|
323
|
-
}
|
|
324
305
|
async forEach(fn, opt = {}) {
|
|
325
|
-
this.transforms.push(
|
|
306
|
+
this.transforms.push(transformMap(fn, {
|
|
326
307
|
predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
|
|
327
308
|
...opt,
|
|
328
309
|
signal: this.abortableSignal,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Transform } from 'node:stream';
|
|
2
|
-
import {
|
|
2
|
+
import { transformMap } from './transformMap.js';
|
|
3
3
|
/**
|
|
4
4
|
* Just a convenience wrapper around `transformMap` that has built-in predicate filtering support.
|
|
5
5
|
*/
|
|
6
6
|
export function transformFilter(asyncPredicate, opt = {}) {
|
|
7
|
-
return
|
|
7
|
+
return transformMap(v => v, {
|
|
8
8
|
asyncPredicate,
|
|
9
9
|
...opt,
|
|
10
10
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AbortableSignal } from '@naturalcycles/js-lib';
|
|
2
2
|
import { ErrorMode } from '@naturalcycles/js-lib/error';
|
|
3
|
-
import { type AbortableAsyncMapper, type AsyncPredicate, END, type PositiveInteger, type Predicate, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
|
|
3
|
+
import { type AbortableAsyncMapper, type AsyncPredicate, END, type NumberOfSeconds, type PositiveInteger, type Predicate, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
|
|
4
4
|
import type { TransformOptions, TransformTyped } from '../stream.model.js';
|
|
5
5
|
export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOptions {
|
|
6
6
|
/**
|
|
@@ -15,22 +15,16 @@ export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOption
|
|
|
15
15
|
/**
|
|
16
16
|
* Number of concurrently pending promises returned by `mapper`.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* It was recently changed up from 16, after some testing that shown that
|
|
20
|
-
* for simple low-cpu mapper functions 32 produces almost 2x throughput.
|
|
21
|
-
* For example, in scenarios like streaming a query from Datastore.
|
|
22
|
-
* UPD: changed back from 32 to 16, "to be on a safe side", as 32 sometimes
|
|
23
|
-
* causes "Datastore timeout errors".
|
|
18
|
+
* @default 16
|
|
24
19
|
*/
|
|
25
20
|
concurrency?: PositiveInteger;
|
|
26
21
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* Time in seconds to gradually increase concurrency from 1 to `concurrency`.
|
|
23
|
+
* Useful for warming up connections to databases, APIs, etc.
|
|
29
24
|
*
|
|
30
|
-
*
|
|
31
|
-
* So, 64 means a total buffer of 128 (64 input and 64 output buffer).
|
|
25
|
+
* Set to 0 to disable warmup (default).
|
|
32
26
|
*/
|
|
33
|
-
|
|
27
|
+
warmupSeconds?: NumberOfSeconds;
|
|
34
28
|
/**
|
|
35
29
|
* @default THROW_IMMEDIATELY
|
|
36
30
|
*/
|
|
@@ -62,6 +56,13 @@ export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOption
|
|
|
62
56
|
*/
|
|
63
57
|
signal?: AbortableSignal;
|
|
64
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Like transformMap, but with native concurrency control (no through2-concurrent dependency)
|
|
61
|
+
* and support for gradual warmup.
|
|
62
|
+
*
|
|
63
|
+
* @experimental
|
|
64
|
+
*/
|
|
65
|
+
export declare function transformMap<IN = any, OUT = IN>(mapper: AbortableAsyncMapper<IN, OUT | typeof SKIP | typeof END>, opt?: TransformMapOptions<IN, OUT>): TransformTyped<IN, OUT>;
|
|
65
66
|
export interface TransformMapStats {
|
|
66
67
|
/**
|
|
67
68
|
* True if transform was successful (didn't throw Immediate or Aggregated error).
|
|
@@ -88,19 +89,6 @@ export interface TransformMapStatsSummary extends TransformMapStats {
|
|
|
88
89
|
*/
|
|
89
90
|
extra?: StringMap<any>;
|
|
90
91
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Like pMap, but for streams.
|
|
93
|
-
* Inspired by `through2`.
|
|
94
|
-
* Main feature is concurrency control (implemented via `through2-concurrent`) and convenient options.
|
|
95
|
-
* Using this allows native stream .pipe() to work and use backpressure.
|
|
96
|
-
*
|
|
97
|
-
* Only works in objectMode (due to through2Concurrent).
|
|
98
|
-
*
|
|
99
|
-
* Concurrency defaults to 16.
|
|
100
|
-
*
|
|
101
|
-
* If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
|
|
102
|
-
*/
|
|
103
|
-
export declare function transformMap<IN = any, OUT = IN>(mapper: AbortableAsyncMapper<IN, OUT | typeof SKIP | typeof END>, opt?: TransformMapOptions<IN, OUT>): TransformTyped<IN, OUT>;
|
|
104
92
|
/**
|
|
105
93
|
* Renders TransformMapStatsSummary into a friendly string,
|
|
106
94
|
* to be used e.g in Github Actions summary or Slack.
|
|
@@ -1,159 +1,175 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
1
2
|
import { _hc } from '@naturalcycles/js-lib';
|
|
2
|
-
import { _since } from '@naturalcycles/js-lib/datetime
|
|
3
|
+
import { _since } from '@naturalcycles/js-lib/datetime';
|
|
3
4
|
import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
|
|
4
5
|
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
|
|
5
|
-
import {
|
|
6
|
+
import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
|
|
7
|
+
import { _stringify } from '@naturalcycles/js-lib/string';
|
|
6
8
|
import { END, SKIP, } from '@naturalcycles/js-lib/types';
|
|
7
|
-
import through2Concurrent from 'through2-concurrent';
|
|
8
9
|
import { yellow } from '../../colors/colors.js';
|
|
9
10
|
import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
|
|
10
|
-
|
|
11
|
-
// export class TransformMap extends AbortableTransform {}
|
|
11
|
+
const WARMUP_CHECK_INTERVAL_MS = 1000;
|
|
12
12
|
/**
|
|
13
|
-
* Like
|
|
14
|
-
*
|
|
15
|
-
* Main feature is concurrency control (implemented via `through2-concurrent`) and convenient options.
|
|
16
|
-
* Using this allows native stream .pipe() to work and use backpressure.
|
|
13
|
+
* Like transformMap, but with native concurrency control (no through2-concurrent dependency)
|
|
14
|
+
* and support for gradual warmup.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Concurrency defaults to 16.
|
|
21
|
-
*
|
|
22
|
-
* If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
|
|
16
|
+
* @experimental
|
|
23
17
|
*/
|
|
24
18
|
export function transformMap(mapper, opt = {}) {
|
|
25
|
-
const { concurrency = 16,
|
|
26
|
-
|
|
27
|
-
const
|
|
19
|
+
const { concurrency: maxConcurrency = 16, warmupSeconds = 0, predicate, asyncPredicate, errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', signal, objectMode = true, highWaterMark = 64, } = opt;
|
|
20
|
+
const warmupMs = warmupSeconds * 1000;
|
|
21
|
+
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
|
|
22
|
+
// Stats
|
|
23
|
+
let started = 0;
|
|
28
24
|
let index = -1;
|
|
29
25
|
let countOut = 0;
|
|
30
26
|
let isSettled = false;
|
|
31
27
|
let ok = true;
|
|
32
28
|
let errors = 0;
|
|
33
|
-
const collectedErrors = [];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const collectedErrors = [];
|
|
30
|
+
// Concurrency control - single counter, single callback for backpressure
|
|
31
|
+
let inFlight = 0;
|
|
32
|
+
let blockedCallback = null;
|
|
33
|
+
let flushBlocked = null;
|
|
34
|
+
// Warmup - cached concurrency to reduce Date.now() syscalls
|
|
35
|
+
let warmupComplete = warmupSeconds <= 0 || maxConcurrency <= 1;
|
|
36
|
+
let concurrency = warmupComplete ? maxConcurrency : 1;
|
|
37
|
+
let lastWarmupCheck = 0;
|
|
38
|
+
return new Transform({
|
|
39
|
+
objectMode,
|
|
37
40
|
readableHighWaterMark: highWaterMark,
|
|
38
41
|
writableHighWaterMark: highWaterMark,
|
|
39
|
-
async
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
ok: false,
|
|
45
|
-
collectedErrors,
|
|
46
|
-
countErrors: errors,
|
|
47
|
-
countIn: index + 1,
|
|
48
|
-
countOut,
|
|
49
|
-
started,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
logger.error(err);
|
|
54
|
-
}
|
|
55
|
-
// emit Aggregated error
|
|
56
|
-
cb(new AggregateError(collectedErrors, `transformMap resulted in ${collectedErrors.length} error(s)`));
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// emit no error
|
|
60
|
-
try {
|
|
61
|
-
await onDone?.({
|
|
62
|
-
ok,
|
|
63
|
-
collectedErrors,
|
|
64
|
-
countErrors: errors,
|
|
65
|
-
countIn: index + 1,
|
|
66
|
-
countOut,
|
|
67
|
-
started,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
logger.error(err);
|
|
72
|
-
}
|
|
73
|
-
cb();
|
|
42
|
+
async transform(chunk, _, cb) {
|
|
43
|
+
// Initialize start time on first item
|
|
44
|
+
if (started === 0) {
|
|
45
|
+
started = Date.now();
|
|
46
|
+
lastWarmupCheck = started;
|
|
74
47
|
}
|
|
75
|
-
},
|
|
76
|
-
}, async function transformMapFn(chunk, _, cb) {
|
|
77
|
-
// Stop processing if isSettled (either THROW_IMMEDIATELY was fired or END received)
|
|
78
|
-
if (isSettled)
|
|
79
|
-
return cb();
|
|
80
|
-
const currentIndex = ++index;
|
|
81
|
-
try {
|
|
82
|
-
const res = await mapper(chunk, currentIndex);
|
|
83
|
-
// Check for isSettled again, as it may happen while mapper was running
|
|
84
48
|
if (isSettled)
|
|
85
49
|
return cb();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
|
|
91
|
-
return cb();
|
|
50
|
+
const currentIndex = ++index;
|
|
51
|
+
inFlight++;
|
|
52
|
+
if (!warmupComplete) {
|
|
53
|
+
updateConcurrency();
|
|
92
54
|
}
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
|
|
55
|
+
// Apply backpressure if at capacity, otherwise request more input
|
|
56
|
+
if (inFlight < concurrency) {
|
|
57
|
+
cb();
|
|
96
58
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
countOut++;
|
|
100
|
-
this.push(res);
|
|
101
|
-
}
|
|
59
|
+
else {
|
|
60
|
+
blockedCallback = cb;
|
|
102
61
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
62
|
+
try {
|
|
63
|
+
const res = await mapper(chunk, currentIndex);
|
|
64
|
+
if (isSettled)
|
|
65
|
+
return;
|
|
66
|
+
if (res === END) {
|
|
67
|
+
isSettled = true;
|
|
68
|
+
logger.log(`transformMap2 END received at index ${currentIndex}`);
|
|
69
|
+
_assert(signal, 'signal is required when using END');
|
|
70
|
+
signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (res === SKIP)
|
|
74
|
+
return;
|
|
75
|
+
let shouldPush = true;
|
|
76
|
+
if (predicate) {
|
|
77
|
+
shouldPush = predicate(res, currentIndex);
|
|
78
|
+
}
|
|
79
|
+
else if (asyncPredicate) {
|
|
80
|
+
shouldPush = (await asyncPredicate(res, currentIndex)) && !isSettled;
|
|
81
|
+
}
|
|
82
|
+
if (shouldPush) {
|
|
106
83
|
countOut++;
|
|
107
84
|
this.push(res);
|
|
108
85
|
}
|
|
109
86
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
87
|
+
catch (err) {
|
|
88
|
+
logger.error(err);
|
|
89
|
+
errors++;
|
|
90
|
+
logErrorStats();
|
|
91
|
+
if (onError) {
|
|
92
|
+
try {
|
|
93
|
+
onError(_anyToError(err), chunk);
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
}
|
|
97
|
+
if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
|
|
98
|
+
isSettled = true;
|
|
99
|
+
ok = false;
|
|
100
|
+
await callOnDone();
|
|
101
|
+
this.destroy(_anyToError(err));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (errorMode === ErrorMode.THROW_AGGREGATED) {
|
|
105
|
+
collectedErrors.push(_anyToError(err));
|
|
106
|
+
}
|
|
113
107
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
finally {
|
|
109
|
+
inFlight--;
|
|
110
|
+
// Release blocked callback if we now have capacity
|
|
111
|
+
if (blockedCallback && inFlight < concurrency) {
|
|
112
|
+
const pendingCb = blockedCallback;
|
|
113
|
+
blockedCallback = null;
|
|
114
|
+
pendingCb();
|
|
115
|
+
}
|
|
116
|
+
// Trigger flush completion if all done
|
|
117
|
+
if (inFlight === 0 && flushBlocked) {
|
|
118
|
+
flushBlocked.resolve();
|
|
123
119
|
}
|
|
124
|
-
catch { }
|
|
125
120
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// await onDone?.({
|
|
133
|
-
// ok: false,
|
|
134
|
-
// collectedErrors,
|
|
135
|
-
// countErrors: errors,
|
|
136
|
-
// countIn: index + 1,
|
|
137
|
-
// countOut,
|
|
138
|
-
// started,
|
|
139
|
-
// })
|
|
140
|
-
// } catch (err) {
|
|
141
|
-
// logger.error(err)
|
|
142
|
-
// }
|
|
143
|
-
return cb(err); // Emit error immediately
|
|
121
|
+
},
|
|
122
|
+
async flush(cb) {
|
|
123
|
+
// Wait for all in-flight operations to complete
|
|
124
|
+
if (inFlight > 0) {
|
|
125
|
+
flushBlocked = pDefer();
|
|
126
|
+
await flushBlocked;
|
|
144
127
|
}
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
logErrorStats(true);
|
|
129
|
+
await callOnDone();
|
|
130
|
+
if (collectedErrors.length) {
|
|
131
|
+
cb(new AggregateError(collectedErrors, `transformMap2 resulted in ${collectedErrors.length} error(s)`));
|
|
147
132
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
133
|
+
else {
|
|
134
|
+
cb();
|
|
135
|
+
}
|
|
136
|
+
},
|
|
151
137
|
});
|
|
138
|
+
function updateConcurrency() {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS)
|
|
141
|
+
return;
|
|
142
|
+
lastWarmupCheck = now;
|
|
143
|
+
const elapsed = now - started;
|
|
144
|
+
if (elapsed >= warmupMs) {
|
|
145
|
+
warmupComplete = true;
|
|
146
|
+
concurrency = maxConcurrency;
|
|
147
|
+
logger.log(`transformMap2: warmup complete in ${_since(started)}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const progress = elapsed / warmupMs;
|
|
151
|
+
concurrency = Math.max(1, Math.floor(1 + (maxConcurrency - 1) * progress));
|
|
152
|
+
}
|
|
152
153
|
function logErrorStats(final = false) {
|
|
153
154
|
if (!errors)
|
|
154
155
|
return;
|
|
155
156
|
logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`);
|
|
156
157
|
}
|
|
158
|
+
async function callOnDone() {
|
|
159
|
+
try {
|
|
160
|
+
await onDone?.({
|
|
161
|
+
ok: collectedErrors.length === 0 && ok,
|
|
162
|
+
collectedErrors,
|
|
163
|
+
countErrors: errors,
|
|
164
|
+
countIn: index + 1,
|
|
165
|
+
countOut,
|
|
166
|
+
started,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
logger.error(err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
157
173
|
}
|
|
158
174
|
/**
|
|
159
175
|
* Renders TransformMapStatsSummary into a friendly string,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
1
2
|
import { Worker } from 'node:worker_threads';
|
|
2
3
|
import { _range } from '@naturalcycles/js-lib/array/range.js';
|
|
3
4
|
import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
|
|
4
|
-
import through2Concurrent from 'through2-concurrent';
|
|
5
5
|
const workerProxyFilePath = `${import.meta.dirname}/workerClassProxy.js`;
|
|
6
6
|
/**
|
|
7
7
|
* Spawns a pool of Workers (threads).
|
|
@@ -21,6 +21,10 @@ export function transformMultiThreaded(opt) {
|
|
|
21
21
|
const workerDonePromises = [];
|
|
22
22
|
const messageDonePromises = {};
|
|
23
23
|
let index = -1; // input chunk index, will start from 0
|
|
24
|
+
// Concurrency control
|
|
25
|
+
let inFlight = 0;
|
|
26
|
+
let blockedCallback = null;
|
|
27
|
+
let flushBlocked = null;
|
|
24
28
|
const workers = _range(0, poolSize).map(workerIndex => {
|
|
25
29
|
workerDonePromises.push(pDefer());
|
|
26
30
|
const worker = new Worker(workerProxyFilePath, {
|
|
@@ -30,20 +34,14 @@ export function transformMultiThreaded(opt) {
|
|
|
30
34
|
...workerData,
|
|
31
35
|
},
|
|
32
36
|
});
|
|
33
|
-
// const {threadId} = worker
|
|
34
|
-
// console.log({threadId})
|
|
35
37
|
worker.on('error', err => {
|
|
36
38
|
console.error(`Worker ${workerIndex} error`, err);
|
|
37
39
|
workerDonePromises[workerIndex].reject(err);
|
|
38
40
|
});
|
|
39
41
|
worker.on('exit', _exitCode => {
|
|
40
|
-
// console.log(`Worker ${index} exit: ${exitCode}`)
|
|
41
42
|
workerDonePromises[workerIndex].resolve(undefined);
|
|
42
43
|
});
|
|
43
44
|
worker.on('message', (out) => {
|
|
44
|
-
// console.log(`Message from Worker ${workerIndex}:`, out)
|
|
45
|
-
// console.log(Object.keys(messageDonePromises))
|
|
46
|
-
// tr.push(out.payload)
|
|
47
45
|
if (out.error) {
|
|
48
46
|
messageDonePromises[out.index].reject(out.error);
|
|
49
47
|
}
|
|
@@ -53,48 +51,70 @@ export function transformMultiThreaded(opt) {
|
|
|
53
51
|
});
|
|
54
52
|
return worker;
|
|
55
53
|
});
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
highWaterMark,
|
|
59
|
-
|
|
54
|
+
return new Transform({
|
|
55
|
+
objectMode: true,
|
|
56
|
+
readableHighWaterMark: highWaterMark,
|
|
57
|
+
writableHighWaterMark: highWaterMark,
|
|
58
|
+
async transform(chunk, _, cb) {
|
|
59
|
+
const currentIndex = ++index;
|
|
60
|
+
inFlight++;
|
|
61
|
+
// Apply backpressure if at capacity, otherwise request more input
|
|
62
|
+
if (inFlight < maxConcurrency) {
|
|
63
|
+
cb();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
blockedCallback = cb;
|
|
67
|
+
}
|
|
68
|
+
// Create the unresolved promise (to await)
|
|
69
|
+
messageDonePromises[currentIndex] = pDefer();
|
|
70
|
+
const worker = workers[currentIndex % poolSize]; // round-robin
|
|
71
|
+
worker.postMessage({
|
|
72
|
+
index: currentIndex,
|
|
73
|
+
payload: chunk,
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const out = await messageDonePromises[currentIndex];
|
|
77
|
+
this.push(out);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Currently we only support ErrorMode.SUPPRESS
|
|
81
|
+
// Error is logged and output continues
|
|
82
|
+
console.error(err);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
delete messageDonePromises[currentIndex];
|
|
86
|
+
inFlight--;
|
|
87
|
+
// Release blocked callback if we now have capacity
|
|
88
|
+
if (blockedCallback && inFlight < maxConcurrency) {
|
|
89
|
+
const pendingCb = blockedCallback;
|
|
90
|
+
blockedCallback = null;
|
|
91
|
+
pendingCb();
|
|
92
|
+
}
|
|
93
|
+
// Trigger flush completion if all done
|
|
94
|
+
if (inFlight === 0 && flushBlocked) {
|
|
95
|
+
flushBlocked.resolve();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async flush(cb) {
|
|
100
|
+
// Wait for all in-flight operations to complete
|
|
101
|
+
if (inFlight > 0) {
|
|
102
|
+
flushBlocked = pDefer();
|
|
103
|
+
await flushBlocked;
|
|
104
|
+
}
|
|
60
105
|
try {
|
|
61
|
-
// Push null (complete) to all
|
|
106
|
+
// Push null (complete) to all workers
|
|
62
107
|
for (const worker of workers) {
|
|
63
108
|
worker.postMessage(null);
|
|
64
109
|
}
|
|
65
|
-
console.log(`transformMultiThreaded.
|
|
110
|
+
console.log(`transformMultiThreaded.flush is waiting for all workers to be done`);
|
|
66
111
|
await Promise.all(workerDonePromises);
|
|
67
|
-
console.log(`transformMultiThreaded.
|
|
112
|
+
console.log(`transformMultiThreaded.flush all workers done`);
|
|
68
113
|
cb();
|
|
69
114
|
}
|
|
70
115
|
catch (err) {
|
|
71
116
|
cb(err);
|
|
72
117
|
}
|
|
73
118
|
},
|
|
74
|
-
}, async function transformMapFn(chunk, _, cb) {
|
|
75
|
-
// Freezing the index, because it may change due to concurrency
|
|
76
|
-
const currentIndex = ++index;
|
|
77
|
-
// Create the unresolved promise (to avait)
|
|
78
|
-
messageDonePromises[currentIndex] = pDefer();
|
|
79
|
-
const worker = workers[currentIndex % poolSize]; // round-robin
|
|
80
|
-
worker.postMessage({
|
|
81
|
-
index: currentIndex,
|
|
82
|
-
payload: chunk,
|
|
83
|
-
});
|
|
84
|
-
try {
|
|
85
|
-
// awaiting for result
|
|
86
|
-
const out = await messageDonePromises[currentIndex];
|
|
87
|
-
// console.log('awaited!')
|
|
88
|
-
// return the result
|
|
89
|
-
cb(null, out);
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
// Currently we only support ErrorMode.SUPPRESS
|
|
93
|
-
// Error is logged and output continues
|
|
94
|
-
console.error(err);
|
|
95
|
-
cb(); // emit nothing in case of an error
|
|
96
|
-
}
|
|
97
|
-
// clean up
|
|
98
|
-
delete messageDonePromises[currentIndex];
|
|
99
119
|
});
|
|
100
120
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.71.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@types/js-yaml": "^4",
|
|
@@ -14,13 +14,11 @@
|
|
|
14
14
|
"js-yaml": "^4",
|
|
15
15
|
"jsonwebtoken": "^9",
|
|
16
16
|
"lru-cache": "^11",
|
|
17
|
-
"through2-concurrent": "^2",
|
|
18
17
|
"tinyglobby": "^0.2",
|
|
19
18
|
"tslib": "^2",
|
|
20
19
|
"yargs": "^18"
|
|
21
20
|
},
|
|
22
21
|
"devDependencies": {
|
|
23
|
-
"@types/through2-concurrent": "^2",
|
|
24
22
|
"@naturalcycles/dev-lib": "18.4.2"
|
|
25
23
|
},
|
|
26
24
|
"exports": {
|
package/src/stream/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ export * from './transform/transformFork.js'
|
|
|
16
16
|
export * from './transform/transformLimit.js'
|
|
17
17
|
export * from './transform/transformLogProgress.js'
|
|
18
18
|
export * from './transform/transformMap.js'
|
|
19
|
-
export * from './transform/transformMap2.js'
|
|
20
19
|
export * from './transform/transformMapSimple.js'
|
|
21
20
|
export * from './transform/transformMapSync.js'
|
|
22
21
|
export * from './transform/transformNoOp.js'
|