@naturalcycles/nodejs-lib 15.70.1 → 15.72.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';
@@ -1,11 +1,10 @@
1
1
  import { type Transform } from 'node:stream';
2
2
  import type { ReadableStream as WebReadableStream } from 'node:stream/web';
3
3
  import { type ZlibOptions, type ZstdOptions } from 'node:zlib';
4
- import { type AbortableAsyncMapper, type AsyncIndexedMapper, type AsyncPredicate, type END, type IndexedMapper, type NonNegativeInteger, type PositiveInteger, type Predicate, type SKIP } from '@naturalcycles/js-lib/types';
4
+ import { type AbortableAsyncMapper, type AsyncIndexedMapper, type AsyncPredicate, type END, type IndexedMapper, type Integer, type NonNegativeInteger, type PositiveInteger, type Predicate, type SKIP } from '@naturalcycles/js-lib/types';
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;
@@ -99,14 +97,21 @@ export declare class Pipeline<T = unknown> {
99
97
  parseJson<TO = unknown>(this: Pipeline<Buffer> | Pipeline<Uint8Array> | Pipeline<string>): Pipeline<TO>;
100
98
  gzip(this: Pipeline<Uint8Array>, opt?: ZlibOptions): Pipeline<Uint8Array>;
101
99
  gunzip(this: Pipeline<Uint8Array>, opt?: ZlibOptions): Pipeline<Uint8Array>;
102
- zstdCompress(this: Pipeline<Uint8Array>, opt?: ZstdOptions): Pipeline<Uint8Array>;
100
+ zstdCompress(this: Pipeline<Uint8Array>, level?: Integer, // defaults to 3
101
+ opt?: ZstdOptions): Pipeline<Uint8Array>;
103
102
  zstdDecompress(this: Pipeline<Uint8Array>, opt?: ZstdOptions): Pipeline<Uint8Array>;
104
103
  toArray(opt?: TransformOptions): Promise<T[]>;
105
104
  toFile(outputFilePath: string): Promise<void>;
106
- toNDJsonFile(outputFilePath: string): Promise<void>;
105
+ /**
106
+ * level corresponds to zstd compression level (if filename ends with .zst),
107
+ * or gzip compression level (if filename ends with .gz).
108
+ * Default levels are:
109
+ * gzip: 6
110
+ * zlib: 3 (optimized for throughput, not size, may be larger than gzip at its default level)
111
+ */
112
+ toNDJsonFile(outputFilePath: string, level?: Integer): Promise<void>;
107
113
  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>;
114
+ forEach(fn: AsyncIndexedMapper<T, void>, opt?: TransformMapOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
110
115
  forEachSync(fn: IndexedMapper<T, void>, opt?: TransformMapSyncOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
111
116
  run(): Promise<void>;
112
117
  }
@@ -4,6 +4,7 @@ import { createGzip, createUnzip, createZstdCompress, createZstdDecompress, } fr
4
4
  import { createAbortableSignal } from '@naturalcycles/js-lib';
5
5
  import { _passthroughPredicate, } from '@naturalcycles/js-lib/types';
6
6
  import { fs2 } from '../fs/fs2.js';
7
+ import { zstdLevelToOptions } from '../zip/zip.util.js';
7
8
  import { createReadStreamAsNDJson } from './ndjson/createReadStreamAsNDJson.js';
8
9
  import { transformJsonParse } from './ndjson/transformJsonParse.js';
9
10
  import { transformToNDJson } from './ndjson/transformToNDJson.js';
@@ -16,7 +17,6 @@ import { transformFork } from './transform/transformFork.js';
16
17
  import { transformLimit } from './transform/transformLimit.js';
17
18
  import { transformLogProgress, } from './transform/transformLogProgress.js';
18
19
  import { transformMap } from './transform/transformMap.js';
19
- import { transformMap2 } from './transform/transformMap2.js';
20
20
  import { transformMapSimple, } from './transform/transformMapSimple.js';
21
21
  import { transformMapSync } from './transform/transformMapSync.js';
22
22
  import { transformOffset } from './transform/transformOffset.js';
@@ -128,15 +128,8 @@ export class Pipeline {
128
128
  this.transforms.push(transformLogProgress(opt));
129
129
  return this;
130
130
  }
131
- mapLegacy(mapper, opt) {
132
- this.transforms.push(transformMap(mapper, {
133
- ...opt,
134
- signal: this.abortableSignal,
135
- }));
136
- return this;
137
- }
138
131
  map(mapper, opt) {
139
- this.transforms.push(transformMap2(mapper, {
132
+ this.transforms.push(transformMap(mapper, {
140
133
  ...opt,
141
134
  signal: this.abortableSignal,
142
135
  }));
@@ -154,7 +147,7 @@ export class Pipeline {
154
147
  return this;
155
148
  }
156
149
  filter(asyncPredicate, opt) {
157
- this.transforms.push(transformMap2(v => v, {
150
+ this.transforms.push(transformMap(v => v, {
158
151
  asyncPredicate,
159
152
  ...opt,
160
153
  signal: this.abortableSignal,
@@ -261,11 +254,9 @@ export class Pipeline {
261
254
  this.objectMode = false;
262
255
  return this;
263
256
  }
264
- zstdCompress(opt) {
265
- this.transforms.push(createZstdCompress({
266
- // chunkSize: 64 * 1024, // no observed speedup
267
- ...opt,
268
- }));
257
+ zstdCompress(level, // defaults to 3
258
+ opt) {
259
+ this.transforms.push(createZstdCompress(zstdLevelToOptions(level, opt)));
269
260
  this.objectMode = false;
270
261
  return this;
271
262
  }
@@ -288,18 +279,24 @@ export class Pipeline {
288
279
  this.destination = fs2.createWriteStream(outputFilePath);
289
280
  await this.run();
290
281
  }
291
- async toNDJsonFile(outputFilePath) {
282
+ /**
283
+ * level corresponds to zstd compression level (if filename ends with .zst),
284
+ * or gzip compression level (if filename ends with .gz).
285
+ * Default levels are:
286
+ * gzip: 6
287
+ * zlib: 3 (optimized for throughput, not size, may be larger than gzip at its default level)
288
+ */
289
+ async toNDJsonFile(outputFilePath, level) {
292
290
  fs2.ensureFile(outputFilePath);
293
291
  this.transforms.push(transformToNDJson());
294
292
  if (outputFilePath.endsWith('.gz')) {
295
293
  this.transforms.push(createGzip({
296
- // chunkSize: 64 * 1024, // no observed speedup
294
+ level,
295
+ // chunkSize: 64 * 1024, // no observed speedup
297
296
  }));
298
297
  }
299
298
  else if (outputFilePath.endsWith('.zst')) {
300
- this.transforms.push(createZstdCompress({
301
- // chunkSize: 64 * 1024, // no observed speedup
302
- }));
299
+ this.transforms.push(createZstdCompress(zstdLevelToOptions(level)));
303
300
  }
304
301
  this.destination = fs2.createWriteStream(outputFilePath, {
305
302
  // highWaterMark: 64 * 1024, // no observed speedup
@@ -310,19 +307,8 @@ export class Pipeline {
310
307
  this.destination = destination;
311
308
  await this.run();
312
309
  }
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
310
  async forEach(fn, opt = {}) {
325
- this.transforms.push(transformMap2(fn, {
311
+ this.transforms.push(transformMap(fn, {
326
312
  predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
327
313
  ...opt,
328
314
  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,