@naturalcycles/nodejs-lib 13.14.0 → 13.16.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/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export * from './stream/readable/readableFromArray';
34
34
  export * from './stream/readable/readableToArray';
35
35
  export * from './stream/stream.model';
36
36
  export * from './stream/progressLogger';
37
- export * from './stream/transform/transformBuffer';
37
+ export * from './stream/transform/transformChunk';
38
38
  export * from './stream/transform/transformFilter';
39
39
  export * from './stream/transform/transformLimit';
40
40
  export * from './stream/transform/transformLogProgress';
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ tslib_1.__exportStar(require("./stream/readable/readableFromArray"), exports);
38
38
  tslib_1.__exportStar(require("./stream/readable/readableToArray"), exports);
39
39
  tslib_1.__exportStar(require("./stream/stream.model"), exports);
40
40
  tslib_1.__exportStar(require("./stream/progressLogger"), exports);
41
- tslib_1.__exportStar(require("./stream/transform/transformBuffer"), exports);
41
+ tslib_1.__exportStar(require("./stream/transform/transformChunk"), exports);
42
42
  tslib_1.__exportStar(require("./stream/transform/transformFilter"), exports);
43
43
  tslib_1.__exportStar(require("./stream/transform/transformLimit"), exports);
44
44
  tslib_1.__exportStar(require("./stream/transform/transformLogProgress"), exports);
@@ -76,14 +76,19 @@ export interface ProgressLoggerCfg<T = any> {
76
76
  * chunk is undefined for "final" stats, otherwise is defined.
77
77
  */
78
78
  extra?: (chunk: T | undefined, index: number) => AnyObject;
79
+ /**
80
+ * Hook that is called when the last item is passed through.
81
+ * Passes the final stats as `ProgressLogItem`.
82
+ */
83
+ onProgressDone?: (stats: ProgressLogItem) => any;
79
84
  /**
80
85
  * If specified - will multiply the counter by this number.
81
- * Useful e.g when using `transformBuffer({ batchSize: 500 })`, so
82
- * it'll accurately represent the number of processed entries (not batches).
86
+ * Useful e.g when using `transformChunk({ chunkSize: 500 })`, so
87
+ * it'll accurately represent the number of processed entries (not chunks).
83
88
  *
84
89
  * Defaults to 1.
85
90
  */
86
- batchSize?: number;
91
+ chunkSize?: number;
87
92
  /**
88
93
  * Experimental logging of item (shunk) sizes, when json-stringified.
89
94
  *
@@ -121,7 +126,7 @@ export declare class ProgressLogger<T> implements Disposable {
121
126
  cfg: ProgressLoggerCfg<T> & {
122
127
  logEvery: number;
123
128
  logSizesBuffer: number;
124
- batchSize: number;
129
+ chunkSize: number;
125
130
  metric: string;
126
131
  logger: CommonLogger;
127
132
  };
@@ -18,7 +18,7 @@ class ProgressLogger {
18
18
  logRPS: true,
19
19
  logEvery: 1000,
20
20
  logSizesBuffer: 100_000,
21
- batchSize: 1,
21
+ chunkSize: 1,
22
22
  logger: console,
23
23
  logProgress: cfg.logProgress !== false && cfg.logEvery !== 0,
24
24
  ...cfg,
@@ -59,11 +59,11 @@ class ProgressLogger {
59
59
  logStats(chunk, final = false, tenx = false) {
60
60
  if (!this.cfg.logProgress)
61
61
  return;
62
- const { metric, extra, batchSize, heapUsed: logHeapUsed, heapTotal: logHeapTotal, rss: logRss, peakRSS: logPeakRss, rssMinusHeap, external, arrayBuffers, logRPS, logger, } = this.cfg;
62
+ const { metric, extra, chunkSize, heapUsed: logHeapUsed, heapTotal: logHeapTotal, rss: logRss, peakRSS: logPeakRss, rssMinusHeap, external, arrayBuffers, logRPS, logger, } = this.cfg;
63
63
  const mem = process.memoryUsage();
64
64
  const now = Date.now();
65
- const batchedProgress = this.progress * batchSize;
66
- const lastRPS = (this.processedLastSecond * batchSize) / ((now - this.lastSecondStarted) / 1000) || 0;
65
+ const batchedProgress = this.progress * chunkSize;
66
+ const lastRPS = (this.processedLastSecond * chunkSize) / ((now - this.lastSecondStarted) / 1000) || 0;
67
67
  const rpsTotal = Math.round(batchedProgress / ((now - this.started) / 1000)) || 0;
68
68
  this.lastSecondStarted = now;
69
69
  this.processedLastSecond = 0;
@@ -107,6 +107,7 @@ class ProgressLogger {
107
107
  }
108
108
  else if (final) {
109
109
  logger.log(`${(0, colors_1.boldWhite)(metric)} took ${(0, colors_1.yellow)((0, js_lib_1._since)(this.started))} to process ${(0, colors_1.yellow)(batchedProgress)} rows with total RPS of ${(0, colors_1.yellow)(rpsTotal)}`);
110
+ this.cfg.onProgressDone?.(o);
110
111
  }
111
112
  }
112
113
  }
@@ -0,0 +1,14 @@
1
+ import { TransformOptions, TransformTyped } from '../stream.model';
2
+ export interface TransformChunkOptions extends TransformOptions {
3
+ /**
4
+ * How many items to include in each chunk.
5
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
6
+ */
7
+ chunkSize: number;
8
+ }
9
+ /**
10
+ * Similar to RxJS bufferCount(),
11
+ * allows to "chunk" the input stream into chunks of `opt.chunkSize` size.
12
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
13
+ */
14
+ export declare function transformChunk<IN = any>(opt: TransformChunkOptions): TransformTyped<IN, IN[]>;
@@ -1,21 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.transformBuffer = void 0;
3
+ exports.transformChunk = void 0;
4
4
  const node_stream_1 = require("node:stream");
5
5
  /**
6
- * Similar to RxJS bufferCount()
7
- *
8
- * @default batchSize is 10
6
+ * Similar to RxJS bufferCount(),
7
+ * allows to "chunk" the input stream into chunks of `opt.chunkSize` size.
8
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
9
9
  */
10
- function transformBuffer(opt) {
11
- const { batchSize } = opt;
10
+ function transformChunk(opt) {
11
+ const { chunkSize } = opt;
12
12
  let buf = [];
13
13
  return new node_stream_1.Transform({
14
14
  objectMode: true,
15
15
  ...opt,
16
16
  transform(chunk, _, cb) {
17
17
  buf.push(chunk);
18
- if (buf.length >= batchSize) {
18
+ if (buf.length >= chunkSize) {
19
19
  cb(null, buf);
20
20
  buf = [];
21
21
  }
@@ -32,4 +32,4 @@ function transformBuffer(opt) {
32
32
  },
33
33
  });
34
34
  }
35
- exports.transformBuffer = transformBuffer;
35
+ exports.transformChunk = transformChunk;
@@ -30,6 +30,17 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
30
30
  * Called BEFORE observable will emit error (unless skipErrors is set to true).
31
31
  */
32
32
  onError?: (err: Error, input: IN) => any;
33
+ /**
34
+ * A hook that is called when the last item is finished processing.
35
+ * stats object is passed, containing countIn and countOut -
36
+ * number of items that entered the transform and number of items that left it.
37
+ *
38
+ * Callback is called **before** [possible] Aggregated error is thrown,
39
+ * and before [possible] THROW_IMMEDIATELY error.
40
+ *
41
+ * onDone callback will be called before Error is thrown.
42
+ */
43
+ onDone?: (stats: TransformMapStats) => any;
33
44
  /**
34
45
  * Progress metric
35
46
  *
@@ -38,6 +49,19 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
38
49
  metric?: string;
39
50
  logger?: CommonLogger;
40
51
  }
52
+ export interface TransformMapStats {
53
+ /**
54
+ * True if transform was successful (didn't throw Immediate or Aggregated error).
55
+ */
56
+ ok: boolean;
57
+ /**
58
+ * Only used (and returned) for ErrorMode.Aggregated
59
+ */
60
+ collectedErrors: Error[];
61
+ countErrors: number;
62
+ countIn: number;
63
+ countOut: number;
64
+ }
41
65
  /**
42
66
  * Like pMap, but for streams.
43
67
  * Inspired by `through2`.
@@ -21,8 +21,9 @@ const stream_util_1 = require("../stream.util");
21
21
  */
22
22
  function transformMap(mapper, opt = {}) {
23
23
  const { concurrency = 16, predicate, // we now default to "no predicate" (meaning pass-everything)
24
- errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput, onError, metric = 'stream', logger = console, } = opt;
24
+ errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput, onError, onDone, metric = 'stream', logger = console, } = opt;
25
25
  let index = -1;
26
+ let countOut = 0;
26
27
  let isSettled = false;
27
28
  let errors = 0;
28
29
  const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
@@ -32,11 +33,25 @@ function transformMap(mapper, opt = {}) {
32
33
  // console.log('transformMap final')
33
34
  logErrorStats(true);
34
35
  if (collectedErrors.length) {
36
+ onDone?.({
37
+ ok: false,
38
+ collectedErrors,
39
+ countErrors: errors,
40
+ countIn: index + 1,
41
+ countOut,
42
+ });
35
43
  // emit Aggregated error
36
44
  cb(new AggregateError(collectedErrors, `transformMap resulted in ${collectedErrors.length} error(s)`));
37
45
  }
38
46
  else {
39
47
  // emit no error
48
+ onDone?.({
49
+ ok: true,
50
+ collectedErrors,
51
+ countErrors: errors,
52
+ countIn: index + 1,
53
+ countOut,
54
+ });
40
55
  cb();
41
56
  }
42
57
  },
@@ -54,6 +69,7 @@ function transformMap(mapper, opt = {}) {
54
69
  }
55
70
  return r !== js_lib_1.SKIP && (!predicate || (await predicate(r, currentIndex)));
56
71
  });
72
+ countOut += passedResults.length;
57
73
  passedResults.forEach(r => this.push(r));
58
74
  if (isSettled) {
59
75
  logger.log(`transformMap END received at index ${currentIndex}`);
@@ -73,6 +89,13 @@ function transformMap(mapper, opt = {}) {
73
89
  }
74
90
  if (errorMode === js_lib_1.ErrorMode.THROW_IMMEDIATELY) {
75
91
  isSettled = true;
92
+ onDone?.({
93
+ ok: false,
94
+ collectedErrors,
95
+ countErrors: errors,
96
+ countIn: index + 1,
97
+ countOut,
98
+ });
76
99
  return cb(err); // Emit error immediately
77
100
  }
78
101
  if (errorMode === js_lib_1.ErrorMode.THROW_AGGREGATED) {
@@ -1,6 +1,7 @@
1
1
  import { CommonLogger, END, ErrorMode, Mapper, Predicate, SKIP } from '@naturalcycles/js-lib';
2
2
  import { AbortableTransform } from '../pipeline/pipeline';
3
3
  import { TransformTyped } from '../stream.model';
4
+ import { TransformMapStats } from './transformMap';
4
5
  export interface TransformMapSyncOptions<IN = any, OUT = IN> {
5
6
  /**
6
7
  * @default true
@@ -28,6 +29,17 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
28
29
  * Called BEFORE observable will emit error (unless skipErrors is set to true).
29
30
  */
30
31
  onError?: (err: Error, input: IN) => any;
32
+ /**
33
+ * A hook that is called when the last item is finished processing.
34
+ * stats object is passed, containing countIn and countOut -
35
+ * number of items that entered the transform and number of items that left it.
36
+ *
37
+ * Callback is called **before** [possible] Aggregated error is thrown,
38
+ * and before [possible] THROW_IMMEDIATELY error.
39
+ *
40
+ * onDone callback will be called before Error is thrown.
41
+ */
42
+ onDone?: (stats: TransformMapStats) => any;
31
43
  /**
32
44
  * Progress metric
33
45
  *
@@ -13,9 +13,10 @@ exports.TransformMapSync = TransformMapSync;
13
13
  * Supposedly faster, for cases when async is not needed.
14
14
  */
15
15
  function transformMapSync(mapper, opt = {}) {
16
- let index = -1;
17
16
  const { predicate, // defaults to "no predicate" (pass everything)
18
- errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput = false, onError, metric = 'stream', objectMode = true, logger = console, } = opt;
17
+ errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput = false, onError, onDone, metric = 'stream', objectMode = true, logger = console, } = opt;
18
+ let index = -1;
19
+ let countOut = 0;
19
20
  let isSettled = false;
20
21
  let errors = 0;
21
22
  const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
@@ -37,6 +38,7 @@ function transformMapSync(mapper, opt = {}) {
37
38
  }
38
39
  return r !== js_lib_1.SKIP && (!predicate || predicate(r, currentIndex));
39
40
  });
41
+ countOut += passedResults.length;
40
42
  passedResults.forEach(r => this.push(r));
41
43
  if (isSettled) {
42
44
  logger.log(`transformMapSync END received at index ${currentIndex}`);
@@ -56,6 +58,13 @@ function transformMapSync(mapper, opt = {}) {
56
58
  }
57
59
  if (errorMode === js_lib_1.ErrorMode.THROW_IMMEDIATELY) {
58
60
  isSettled = true;
61
+ onDone?.({
62
+ ok: false,
63
+ collectedErrors,
64
+ countErrors: errors,
65
+ countIn: index + 1,
66
+ countOut,
67
+ });
59
68
  // Emit error immediately
60
69
  return cb(err);
61
70
  }
@@ -69,11 +78,25 @@ function transformMapSync(mapper, opt = {}) {
69
78
  // console.log('transformMap final')
70
79
  logErrorStats(true);
71
80
  if (collectedErrors.length) {
81
+ onDone?.({
82
+ ok: false,
83
+ collectedErrors,
84
+ countErrors: errors,
85
+ countIn: index + 1,
86
+ countOut,
87
+ });
72
88
  // emit Aggregated error
73
89
  cb(new AggregateError(collectedErrors, `transformMapSync resulted in ${collectedErrors.length} error(s)`));
74
90
  }
75
91
  else {
76
92
  // emit no error
93
+ onDone?.({
94
+ ok: true,
95
+ collectedErrors,
96
+ countErrors: errors,
97
+ countIn: index + 1,
98
+ countOut,
99
+ });
77
100
  cb();
78
101
  }
79
102
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
- "version": "13.14.0",
3
+ "version": "13.16.0",
4
4
  "scripts": {
5
5
  "prepare": "husky",
6
6
  "docs-serve": "vuepress dev docs",
package/src/index.ts CHANGED
@@ -44,7 +44,7 @@ export * from './stream/readable/readableFromArray'
44
44
  export * from './stream/readable/readableToArray'
45
45
  export * from './stream/stream.model'
46
46
  export * from './stream/progressLogger'
47
- export * from './stream/transform/transformBuffer'
47
+ export * from './stream/transform/transformChunk'
48
48
  export * from './stream/transform/transformFilter'
49
49
  export * from './stream/transform/transformLimit'
50
50
  export * from './stream/transform/transformLogProgress'
@@ -101,14 +101,20 @@ export interface ProgressLoggerCfg<T = any> {
101
101
  */
102
102
  extra?: (chunk: T | undefined, index: number) => AnyObject
103
103
 
104
+ /**
105
+ * Hook that is called when the last item is passed through.
106
+ * Passes the final stats as `ProgressLogItem`.
107
+ */
108
+ onProgressDone?: (stats: ProgressLogItem) => any
109
+
104
110
  /**
105
111
  * If specified - will multiply the counter by this number.
106
- * Useful e.g when using `transformBuffer({ batchSize: 500 })`, so
107
- * it'll accurately represent the number of processed entries (not batches).
112
+ * Useful e.g when using `transformChunk({ chunkSize: 500 })`, so
113
+ * it'll accurately represent the number of processed entries (not chunks).
108
114
  *
109
115
  * Defaults to 1.
110
116
  */
111
- batchSize?: number
117
+ chunkSize?: number
112
118
 
113
119
  /**
114
120
  * Experimental logging of item (shunk) sizes, when json-stringified.
@@ -160,7 +166,7 @@ export class ProgressLogger<T> implements Disposable {
160
166
  logRPS: true,
161
167
  logEvery: 1000,
162
168
  logSizesBuffer: 100_000,
163
- batchSize: 1,
169
+ chunkSize: 1,
164
170
  logger: console,
165
171
  logProgress: cfg.logProgress !== false && cfg.logEvery !== 0,
166
172
  ...cfg,
@@ -174,7 +180,7 @@ export class ProgressLogger<T> implements Disposable {
174
180
  cfg!: ProgressLoggerCfg<T> & {
175
181
  logEvery: number
176
182
  logSizesBuffer: number
177
- batchSize: number
183
+ chunkSize: number
178
184
  metric: string
179
185
  logger: CommonLogger
180
186
  }
@@ -230,7 +236,7 @@ export class ProgressLogger<T> implements Disposable {
230
236
  const {
231
237
  metric,
232
238
  extra,
233
- batchSize,
239
+ chunkSize,
234
240
  heapUsed: logHeapUsed,
235
241
  heapTotal: logHeapTotal,
236
242
  rss: logRss,
@@ -245,9 +251,9 @@ export class ProgressLogger<T> implements Disposable {
245
251
  const mem = process.memoryUsage()
246
252
 
247
253
  const now = Date.now()
248
- const batchedProgress = this.progress * batchSize
254
+ const batchedProgress = this.progress * chunkSize
249
255
  const lastRPS =
250
- (this.processedLastSecond * batchSize) / ((now - this.lastSecondStarted) / 1000) || 0
256
+ (this.processedLastSecond * chunkSize) / ((now - this.lastSecondStarted) / 1000) || 0
251
257
  const rpsTotal = Math.round(batchedProgress / ((now - this.started) / 1000)) || 0
252
258
  this.lastSecondStarted = now
253
259
  this.processedLastSecond = 0
@@ -298,6 +304,8 @@ export class ProgressLogger<T> implements Disposable {
298
304
  batchedProgress,
299
305
  )} rows with total RPS of ${yellow(rpsTotal)}`,
300
306
  )
307
+
308
+ this.cfg.onProgressDone?.(o)
301
309
  }
302
310
  }
303
311
  }
@@ -0,0 +1,44 @@
1
+ import { Transform } from 'node:stream'
2
+ import { TransformOptions, TransformTyped } from '../stream.model'
3
+
4
+ export interface TransformChunkOptions extends TransformOptions {
5
+ /**
6
+ * How many items to include in each chunk.
7
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
8
+ */
9
+ chunkSize: number
10
+ }
11
+
12
+ /**
13
+ * Similar to RxJS bufferCount(),
14
+ * allows to "chunk" the input stream into chunks of `opt.chunkSize` size.
15
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
16
+ */
17
+ export function transformChunk<IN = any>(opt: TransformChunkOptions): TransformTyped<IN, IN[]> {
18
+ const { chunkSize } = opt
19
+
20
+ let buf: IN[] = []
21
+
22
+ return new Transform({
23
+ objectMode: true,
24
+ ...opt,
25
+ transform(chunk, _, cb) {
26
+ buf.push(chunk)
27
+
28
+ if (buf.length >= chunkSize) {
29
+ cb(null, buf)
30
+ buf = []
31
+ } else {
32
+ cb()
33
+ }
34
+ },
35
+ final(this: Transform, cb) {
36
+ if (buf.length) {
37
+ this.push(buf)
38
+ buf = []
39
+ }
40
+
41
+ cb()
42
+ },
43
+ })
44
+ }
@@ -49,6 +49,18 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
49
49
  */
50
50
  onError?: (err: Error, input: IN) => any
51
51
 
52
+ /**
53
+ * A hook that is called when the last item is finished processing.
54
+ * stats object is passed, containing countIn and countOut -
55
+ * number of items that entered the transform and number of items that left it.
56
+ *
57
+ * Callback is called **before** [possible] Aggregated error is thrown,
58
+ * and before [possible] THROW_IMMEDIATELY error.
59
+ *
60
+ * onDone callback will be called before Error is thrown.
61
+ */
62
+ onDone?: (stats: TransformMapStats) => any
63
+
52
64
  /**
53
65
  * Progress metric
54
66
  *
@@ -59,6 +71,20 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
59
71
  logger?: CommonLogger
60
72
  }
61
73
 
74
+ export interface TransformMapStats {
75
+ /**
76
+ * True if transform was successful (didn't throw Immediate or Aggregated error).
77
+ */
78
+ ok: boolean
79
+ /**
80
+ * Only used (and returned) for ErrorMode.Aggregated
81
+ */
82
+ collectedErrors: Error[]
83
+ countErrors: number
84
+ countIn: number
85
+ countOut: number
86
+ }
87
+
62
88
  // doesn't work, cause here we don't construct our Transform instance ourselves
63
89
  // export class TransformMap extends AbortableTransform {}
64
90
 
@@ -84,11 +110,13 @@ export function transformMap<IN = any, OUT = IN>(
84
110
  errorMode = ErrorMode.THROW_IMMEDIATELY,
85
111
  flattenArrayOutput,
86
112
  onError,
113
+ onDone,
87
114
  metric = 'stream',
88
115
  logger = console,
89
116
  } = opt
90
117
 
91
118
  let index = -1
119
+ let countOut = 0
92
120
  let isSettled = false
93
121
  let errors = 0
94
122
  const collectedErrors: Error[] = [] // only used if errorMode == THROW_AGGREGATED
@@ -102,6 +130,14 @@ export function transformMap<IN = any, OUT = IN>(
102
130
  logErrorStats(true)
103
131
 
104
132
  if (collectedErrors.length) {
133
+ onDone?.({
134
+ ok: false,
135
+ collectedErrors,
136
+ countErrors: errors,
137
+ countIn: index + 1,
138
+ countOut,
139
+ })
140
+
105
141
  // emit Aggregated error
106
142
  cb(
107
143
  new AggregateError(
@@ -111,6 +147,15 @@ export function transformMap<IN = any, OUT = IN>(
111
147
  )
112
148
  } else {
113
149
  // emit no error
150
+
151
+ onDone?.({
152
+ ok: true,
153
+ collectedErrors,
154
+ countErrors: errors,
155
+ countIn: index + 1,
156
+ countOut,
157
+ })
158
+
114
159
  cb()
115
160
  }
116
161
  },
@@ -134,6 +179,7 @@ export function transformMap<IN = any, OUT = IN>(
134
179
  },
135
180
  )
136
181
 
182
+ countOut += passedResults.length
137
183
  passedResults.forEach(r => this.push(r))
138
184
 
139
185
  if (isSettled) {
@@ -155,6 +201,13 @@ export function transformMap<IN = any, OUT = IN>(
155
201
 
156
202
  if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
157
203
  isSettled = true
204
+ onDone?.({
205
+ ok: false,
206
+ collectedErrors,
207
+ countErrors: errors,
208
+ countIn: index + 1,
209
+ countOut,
210
+ })
158
211
  return cb(err) // Emit error immediately
159
212
  }
160
213
 
@@ -11,6 +11,7 @@ import { yellow } from '../../colors/colors'
11
11
  import { AbortableTransform } from '../pipeline/pipeline'
12
12
  import { TransformTyped } from '../stream.model'
13
13
  import { pipelineClose } from '../stream.util'
14
+ import { TransformMapStats } from './transformMap'
14
15
 
15
16
  export interface TransformMapSyncOptions<IN = any, OUT = IN> {
16
17
  /**
@@ -44,6 +45,18 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
44
45
  */
45
46
  onError?: (err: Error, input: IN) => any
46
47
 
48
+ /**
49
+ * A hook that is called when the last item is finished processing.
50
+ * stats object is passed, containing countIn and countOut -
51
+ * number of items that entered the transform and number of items that left it.
52
+ *
53
+ * Callback is called **before** [possible] Aggregated error is thrown,
54
+ * and before [possible] THROW_IMMEDIATELY error.
55
+ *
56
+ * onDone callback will be called before Error is thrown.
57
+ */
58
+ onDone?: (stats: TransformMapStats) => any
59
+
47
60
  /**
48
61
  * Progress metric
49
62
  *
@@ -64,17 +77,19 @@ export function transformMapSync<IN = any, OUT = IN>(
64
77
  mapper: Mapper<IN, OUT | typeof SKIP | typeof END>,
65
78
  opt: TransformMapSyncOptions = {},
66
79
  ): TransformTyped<IN, OUT> {
67
- let index = -1
68
-
69
80
  const {
70
81
  predicate, // defaults to "no predicate" (pass everything)
71
82
  errorMode = ErrorMode.THROW_IMMEDIATELY,
72
83
  flattenArrayOutput = false,
73
84
  onError,
85
+ onDone,
74
86
  metric = 'stream',
75
87
  objectMode = true,
76
88
  logger = console,
77
89
  } = opt
90
+
91
+ let index = -1
92
+ let countOut = 0
78
93
  let isSettled = false
79
94
  let errors = 0
80
95
  const collectedErrors: Error[] = [] // only used if errorMode == THROW_AGGREGATED
@@ -100,6 +115,7 @@ export function transformMapSync<IN = any, OUT = IN>(
100
115
  return r !== SKIP && (!predicate || predicate(r, currentIndex))
101
116
  })
102
117
 
118
+ countOut += passedResults.length
103
119
  passedResults.forEach(r => this.push(r))
104
120
 
105
121
  if (isSettled) {
@@ -122,6 +138,13 @@ export function transformMapSync<IN = any, OUT = IN>(
122
138
 
123
139
  if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
124
140
  isSettled = true
141
+ onDone?.({
142
+ ok: false,
143
+ collectedErrors,
144
+ countErrors: errors,
145
+ countIn: index + 1,
146
+ countOut,
147
+ })
125
148
  // Emit error immediately
126
149
  return cb(err as Error)
127
150
  }
@@ -139,6 +162,14 @@ export function transformMapSync<IN = any, OUT = IN>(
139
162
  logErrorStats(true)
140
163
 
141
164
  if (collectedErrors.length) {
165
+ onDone?.({
166
+ ok: false,
167
+ collectedErrors,
168
+ countErrors: errors,
169
+ countIn: index + 1,
170
+ countOut,
171
+ })
172
+
142
173
  // emit Aggregated error
143
174
  cb(
144
175
  new AggregateError(
@@ -148,6 +179,15 @@ export function transformMapSync<IN = any, OUT = IN>(
148
179
  )
149
180
  } else {
150
181
  // emit no error
182
+
183
+ onDone?.({
184
+ ok: true,
185
+ collectedErrors,
186
+ countErrors: errors,
187
+ countIn: index + 1,
188
+ countOut,
189
+ })
190
+
151
191
  cb()
152
192
  }
153
193
  },
@@ -1,10 +0,0 @@
1
- import { TransformOptions, TransformTyped } from '../stream.model';
2
- export interface TransformBufferOptions extends TransformOptions {
3
- batchSize: number;
4
- }
5
- /**
6
- * Similar to RxJS bufferCount()
7
- *
8
- * @default batchSize is 10
9
- */
10
- export declare function transformBuffer<IN = any>(opt: TransformBufferOptions): TransformTyped<IN, IN[]>;
@@ -1,40 +0,0 @@
1
- import { Transform } from 'node:stream'
2
- import { TransformOptions, TransformTyped } from '../stream.model'
3
-
4
- export interface TransformBufferOptions extends TransformOptions {
5
- batchSize: number
6
- }
7
-
8
- /**
9
- * Similar to RxJS bufferCount()
10
- *
11
- * @default batchSize is 10
12
- */
13
- export function transformBuffer<IN = any>(opt: TransformBufferOptions): TransformTyped<IN, IN[]> {
14
- const { batchSize } = opt
15
-
16
- let buf: IN[] = []
17
-
18
- return new Transform({
19
- objectMode: true,
20
- ...opt,
21
- transform(chunk, _, cb) {
22
- buf.push(chunk)
23
-
24
- if (buf.length >= batchSize) {
25
- cb(null, buf)
26
- buf = []
27
- } else {
28
- cb()
29
- }
30
- },
31
- final(this: Transform, cb) {
32
- if (buf.length) {
33
- this.push(buf)
34
- buf = []
35
- }
36
-
37
- cb()
38
- },
39
- })
40
- }