@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.
@@ -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';
@@ -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
- mapLegacy<TO>(mapper: AbortableAsyncMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMapOptions<T, TO>): Pipeline<TO>;
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
- forEachLegacy(fn: AsyncIndexedMapper<T, void>, opt?: TransformMapOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
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
  }
@@ -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(transformMap2(mapper, {
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(transformMap2(v => v, {
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(transformMap2(fn, {
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 { transformMap2 } from './transformMap2.js';
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 transformMap2(v => v, {
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
- * Default is 16.
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
- * Defaults to 64 items.
28
- * (objectMode default is 16, but we increased it)
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
- * Affects both readable and writable highWaterMark (buffer).
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
- highWaterMark?: PositiveInteger;
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/time.util.js';
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 { _stringify } from '@naturalcycles/js-lib/string/stringify.js';
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
- // doesn't work, cause here we don't construct our Transform instance ourselves
11
- // export class TransformMap extends AbortableTransform {}
11
+ const WARMUP_CHECK_INTERVAL_MS = 1000;
12
12
  /**
13
- * Like pMap, but for streams.
14
- * Inspired by `through2`.
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
- * Only works in objectMode (due to through2Concurrent).
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, highWaterMark = 64, predicate, // we now default to "no predicate" (meaning pass-everything)
26
- asyncPredicate, errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', signal, } = opt;
27
- const started = Date.now();
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 = []; // only used if errorMode == THROW_AGGREGATED
34
- const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
35
- return through2Concurrent.obj({
36
- maxConcurrency: concurrency,
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 final(cb) {
40
- logErrorStats(true);
41
- if (collectedErrors.length) {
42
- try {
43
- await onDone?.({
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
- if (res === END) {
87
- isSettled = true;
88
- logger.log(`transformMap END received at index ${currentIndex}`);
89
- _assert(signal, 'signal is required when using END');
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 (res === SKIP) {
94
- // do nothing, don't push
95
- return cb();
55
+ // Apply backpressure if at capacity, otherwise request more input
56
+ if (inFlight < concurrency) {
57
+ cb();
96
58
  }
97
- if (predicate) {
98
- if (predicate(res, currentIndex)) {
99
- countOut++;
100
- this.push(res);
101
- }
59
+ else {
60
+ blockedCallback = cb;
102
61
  }
103
- else if (asyncPredicate) {
104
- if ((await asyncPredicate(res, currentIndex)) && !isSettled) {
105
- // isSettled could have happened in parallel, hence the extra check
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
- else {
111
- countOut++;
112
- this.push(res);
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
- cb(); // done processing
115
- }
116
- catch (err) {
117
- logger.error(err);
118
- errors++;
119
- logErrorStats();
120
- if (onError) {
121
- try {
122
- onError(_anyToError(err), chunk);
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
- if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
127
- isSettled = true;
128
- ok = false;
129
- // Tests show that onDone is still called at `final` (second time),
130
- // so, we no longer call it here
131
- // try {
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
- if (errorMode === ErrorMode.THROW_AGGREGATED) {
146
- collectedErrors.push(err);
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
- // Tell input stream that we're done processing, but emit nothing to output - not error nor result
149
- cb();
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 through2Concurrent.obj({
57
- maxConcurrency,
58
- highWaterMark,
59
- async final(cb) {
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 sub-streams
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.final is waiting for all chains to be done`);
110
+ console.log(`transformMultiThreaded.flush is waiting for all workers to be done`);
66
111
  await Promise.all(workerDonePromises);
67
- console.log(`transformMultiThreaded.final all chains done`);
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.70.1",
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": {
@@ -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'