@naturalcycles/nodejs-lib 12.55.1 → 12.58.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.
Files changed (35) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +3 -3
  3. package/dist/stream/ndjson/ndJsonFileRead.js +1 -1
  4. package/dist/stream/transform/{beta/transformMap2.d.ts → legacy/transformMap.d.ts} +1 -1
  5. package/dist/stream/transform/legacy/transformMap.js +94 -0
  6. package/dist/stream/transform/transformLimit.d.ts +5 -1
  7. package/dist/stream/transform/transformLimit.js +2 -1
  8. package/dist/stream/transform/transformLogProgress.d.ts +2 -1
  9. package/dist/stream/transform/transformLogProgress.js +4 -4
  10. package/dist/stream/transform/transformMap.d.ts +6 -1
  11. package/dist/stream/transform/transformMap.js +62 -48
  12. package/dist/stream/transform/transformMapSimple.d.ts +2 -1
  13. package/dist/stream/transform/transformMapSimple.js +2 -2
  14. package/dist/stream/transform/transformMapSync.d.ts +2 -1
  15. package/dist/stream/transform/transformMapSync.js +3 -3
  16. package/dist/stream/transform/transformTap.d.ts +5 -2
  17. package/dist/stream/transform/transformTap.js +2 -1
  18. package/dist/stream/writable/writableForEach.d.ts +5 -1
  19. package/dist/stream/writable/writableForEach.js +8 -1
  20. package/dist/stream/writable/writableFork.d.ts +2 -0
  21. package/dist/stream/writable/writableFork.js +2 -0
  22. package/package.json +1 -1
  23. package/src/index.ts +1 -2
  24. package/src/stream/ndjson/ndJsonFileRead.ts +2 -2
  25. package/src/stream/transform/legacy/transformMap.ts +133 -0
  26. package/src/stream/transform/transformLimit.ts +8 -2
  27. package/src/stream/transform/transformLogProgress.ts +7 -4
  28. package/src/stream/transform/transformMap.ts +79 -67
  29. package/src/stream/transform/transformMapSimple.ts +5 -3
  30. package/src/stream/transform/transformMapSync.ts +6 -3
  31. package/src/stream/transform/transformTap.ts +8 -3
  32. package/src/stream/writable/writableForEach.ts +12 -2
  33. package/src/stream/writable/writableFork.ts +2 -0
  34. package/dist/stream/transform/beta/transformMap2.js +0 -98
  35. package/src/stream/transform/beta/transformMap2.ts +0 -134
@@ -0,0 +1,133 @@
1
+ import { Transform } from 'stream'
2
+ import {
3
+ AggregatedError,
4
+ AsyncMapper,
5
+ CommonLogger,
6
+ ErrorMode,
7
+ pFilter,
8
+ } from '@naturalcycles/js-lib'
9
+ import through2Concurrent = require('through2-concurrent')
10
+ import { yellow } from '../../../colors'
11
+ import { TransformTyped } from '../../stream.model'
12
+ import { TransformMapOptions } from '../transformMap'
13
+
14
+ export function notNullishPredicate(item: any): boolean {
15
+ return item !== undefined && item !== null
16
+ }
17
+
18
+ /**
19
+ * Like pMap, but for streams.
20
+ * Inspired by `through2`.
21
+ * Main feature is concurrency control (implemented via `through2-concurrent`) and convenient options.
22
+ * Using this allows native stream .pipe() to work and use backpressure.
23
+ *
24
+ * Only works in objectMode (due to through2Concurrent).
25
+ *
26
+ * Concurrency defaults to 16.
27
+ *
28
+ * If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
29
+ */
30
+ export function transformMapLegacy<IN = any, OUT = IN>(
31
+ mapper: AsyncMapper<IN, OUT>,
32
+ opt: TransformMapOptions<IN, OUT> = {},
33
+ ): TransformTyped<IN, OUT> {
34
+ const {
35
+ concurrency = 16,
36
+ predicate = notNullishPredicate,
37
+ errorMode = ErrorMode.THROW_IMMEDIATELY,
38
+ flattenArrayOutput,
39
+ onError,
40
+ beforeFinal,
41
+ metric = 'stream',
42
+ logger = console,
43
+ } = opt
44
+
45
+ let index = -1
46
+ let isRejected = false
47
+ let errors = 0
48
+ const collectedErrors: Error[] = [] // only used if errorMode == THROW_AGGREGATED
49
+
50
+ return through2Concurrent.obj(
51
+ {
52
+ maxConcurrency: concurrency,
53
+ // autoDestroy: true,
54
+ async final(cb) {
55
+ // console.log('transformMap final')
56
+
57
+ logErrorStats(logger, true)
58
+
59
+ await beforeFinal?.() // call beforeFinal if defined
60
+
61
+ if (collectedErrors.length) {
62
+ // emit Aggregated error
63
+ cb(new AggregatedError(collectedErrors))
64
+ } else {
65
+ // emit no error
66
+ cb()
67
+ }
68
+ },
69
+ },
70
+ async function transformMapFn(
71
+ this: Transform,
72
+ chunk: IN,
73
+ _encoding: any,
74
+ cb: (...args: any[]) => any,
75
+ ) {
76
+ index++
77
+ // console.log({chunk, _encoding})
78
+
79
+ // Stop processing if THROW_IMMEDIATELY mode is used
80
+ if (isRejected && errorMode === ErrorMode.THROW_IMMEDIATELY) return cb()
81
+
82
+ try {
83
+ const currentIndex = index // because we need to pass it to 2 functions - mapper and predicate. Refers to INPUT index (since it may return multiple outputs)
84
+ const res = await mapper(chunk, currentIndex)
85
+ const passedResults = await pFilter(
86
+ flattenArrayOutput && Array.isArray(res) ? res : [res],
87
+ async r => await predicate(r, currentIndex),
88
+ )
89
+
90
+ if (passedResults.length === 0) {
91
+ cb() // 0 results
92
+ } else {
93
+ passedResults.forEach(r => {
94
+ this.push(r)
95
+ // cb(null, r)
96
+ })
97
+ cb() // done processing
98
+ }
99
+ } catch (err) {
100
+ logger.error(err)
101
+
102
+ errors++
103
+
104
+ logErrorStats(logger)
105
+
106
+ if (onError) {
107
+ try {
108
+ onError(err, chunk)
109
+ } catch {}
110
+ }
111
+
112
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
113
+ isRejected = true
114
+ // Emit error immediately
115
+ return cb(err)
116
+ }
117
+
118
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
119
+ collectedErrors.push(err as Error)
120
+ }
121
+
122
+ // Tell input stream that we're done processing, but emit nothing to output - not error nor result
123
+ cb()
124
+ }
125
+ },
126
+ )
127
+
128
+ function logErrorStats(logger: CommonLogger, final = false): void {
129
+ if (!errors) return
130
+
131
+ logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`)
132
+ }
133
+ }
@@ -1,13 +1,19 @@
1
1
  import { Transform } from 'stream'
2
+ import { CommonLogger } from '@naturalcycles/js-lib'
2
3
  import { TransformOptions, TransformTyped } from '../stream.model'
3
4
 
5
+ export interface TransformLimitOptions extends TransformOptions {
6
+ logger?: CommonLogger
7
+ }
8
+
4
9
  /**
5
10
  * 0 or falsy value means "no limit"
6
11
  */
7
12
  export function transformLimit<IN>(
8
13
  limit?: number,
9
- opt: TransformOptions = {},
14
+ opt: TransformLimitOptions = {},
10
15
  ): TransformTyped<IN, IN> {
16
+ const { logger = console } = opt
11
17
  let index = 0
12
18
  let ended = false
13
19
 
@@ -25,7 +31,7 @@ export function transformLimit<IN>(
25
31
 
26
32
  if (limit && index === limit) {
27
33
  ended = true
28
- console.log(`transformLimit: limit of ${limit} reached`)
34
+ logger.log(`transformLimit: limit of ${limit} reached`)
29
35
  // this.emit('end') // this makes it "halt" on Node 14 lts
30
36
  }
31
37
  },
@@ -1,6 +1,6 @@
1
1
  import { Transform } from 'stream'
2
2
  import { inspect, InspectOptions } from 'util'
3
- import { SimpleMovingAverage, _mb, _since, AnyObject } from '@naturalcycles/js-lib'
3
+ import { SimpleMovingAverage, _mb, _since, AnyObject, CommonLogger } from '@naturalcycles/js-lib'
4
4
  import { dayjs } from '@naturalcycles/time-lib'
5
5
  import { boldWhite, dimGrey, white, yellow } from '../../colors'
6
6
  import { hasColors } from '../../colors/colors'
@@ -86,6 +86,8 @@ export interface TransformLogProgressOptions<IN = any> extends TransformOptions
86
86
  */
87
87
  logEvery?: number
88
88
 
89
+ logger?: CommonLogger
90
+
89
91
  /**
90
92
  * Function to return extra properties to the "progress object".
91
93
  *
@@ -124,6 +126,7 @@ export function transformLogProgress<IN = any>(
124
126
  logEvery = 1000,
125
127
  batchSize = 1,
126
128
  extra,
129
+ logger = console,
127
130
  } = opt
128
131
  const logProgress = opt.logProgress !== false && logEvery !== 0 // true by default
129
132
  const logEvery10 = logEvery * 10
@@ -172,7 +175,7 @@ export function transformLogProgress<IN = any>(
172
175
  const rps10 = Math.round(sma.push(lastRPS))
173
176
  if (mem.rss > peakRSS) peakRSS = mem.rss
174
177
 
175
- console.log(
178
+ logger.log(
176
179
  inspect(
177
180
  {
178
181
  [final ? `${metric}_final` : metric]: batchedProgress,
@@ -202,13 +205,13 @@ export function transformLogProgress<IN = any>(
202
205
  perHour = Math.round(perHour / 1000) + 'K'
203
206
  }
204
207
 
205
- console.log(
208
+ logger.log(
206
209
  `${dimGrey(dayjs().toPretty())} ${white(metric)} took ${yellow(
207
210
  _since(started),
208
211
  )} so far to process ${yellow(batchedProgress)} rows, ~${yellow(perHour)}/hour`,
209
212
  )
210
213
  } else if (final) {
211
- console.log(
214
+ logger.log(
212
215
  `${boldWhite(metric)} took ${yellow(_since(started))} to process ${yellow(
213
216
  batchedProgress,
214
217
  )} rows with total RPS of ${yellow(rpsTotal)}`,
@@ -6,10 +6,11 @@ import {
6
6
  CommonLogger,
7
7
  ErrorMode,
8
8
  pFilter,
9
+ PQueue,
9
10
  } from '@naturalcycles/js-lib'
10
- import through2Concurrent = require('through2-concurrent')
11
11
  import { yellow } from '../../colors'
12
12
  import { TransformTyped } from '../stream.model'
13
+ import { transformMapLegacy } from './legacy/transformMap'
13
14
 
14
15
  export interface TransformMapOptions<IN = any, OUT = IN> {
15
16
  /**
@@ -66,6 +67,11 @@ export function notNullishPredicate(item: any): boolean {
66
67
  return item !== undefined && item !== null
67
68
  }
68
69
 
70
+ /**
71
+ * Temporary export legacy transformMap, to debug 503 errors
72
+ */
73
+ export const transformMap = transformMapLegacy
74
+
69
75
  /**
70
76
  * Like pMap, but for streams.
71
77
  * Inspired by `through2`.
@@ -78,7 +84,7 @@ export function notNullishPredicate(item: any): boolean {
78
84
  *
79
85
  * If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
80
86
  */
81
- export function transformMap<IN = any, OUT = IN>(
87
+ export function transformMapNew<IN = any, OUT = IN>(
82
88
  mapper: AsyncMapper<IN, OUT>,
83
89
  opt: TransformMapOptions<IN, OUT> = {},
84
90
  ): TransformTyped<IN, OUT> {
@@ -98,83 +104,89 @@ export function transformMap<IN = any, OUT = IN>(
98
104
  let errors = 0
99
105
  const collectedErrors: Error[] = [] // only used if errorMode == THROW_AGGREGATED
100
106
 
101
- return through2Concurrent.obj(
102
- {
103
- maxConcurrency: concurrency,
104
- // autoDestroy: true,
105
- async final(cb) {
106
- // console.log('transformMap final')
107
+ const q = new PQueue({
108
+ concurrency,
109
+ resolveOn: 'start',
110
+ // debug: true,
111
+ })
107
112
 
108
- logErrorStats(logger, true)
113
+ return new Transform({
114
+ objectMode: true,
109
115
 
110
- await beforeFinal?.() // call beforeFinal if defined
116
+ async final(cb) {
117
+ // console.log('transformMap final', {index}, q.inFlight, q.queueSize)
111
118
 
112
- if (collectedErrors.length) {
113
- // emit Aggregated error
114
- cb(new AggregatedError(collectedErrors))
115
- } else {
116
- // emit no error
117
- cb()
118
- }
119
- },
120
- },
121
- async function transformMapFn(
122
- this: Transform,
123
- chunk: IN,
124
- _encoding: any,
125
- cb: (...args: any[]) => any,
126
- ) {
127
- index++
128
- // console.log({chunk, _encoding})
129
-
130
- // Stop processing if THROW_IMMEDIATELY mode is used
131
- if (isRejected && errorMode === ErrorMode.THROW_IMMEDIATELY) return cb()
119
+ // wait for the current inFlight jobs to complete and push their results
120
+ await q.onIdle()
132
121
 
133
- try {
134
- const currentIndex = index // because we need to pass it to 2 functions - mapper and predicate. Refers to INPUT index (since it may return multiple outputs)
135
- const res = await mapper(chunk, currentIndex)
136
- const passedResults = await pFilter(
137
- flattenArrayOutput && Array.isArray(res) ? res : [res],
138
- async r => await predicate(r, currentIndex),
139
- )
140
-
141
- if (passedResults.length === 0) {
142
- cb() // 0 results
143
- } else {
144
- passedResults.forEach(r => {
145
- this.push(r)
146
- // cb(null, r)
147
- })
148
- cb() // done processing
149
- }
150
- } catch (err) {
151
- logger.error(err)
122
+ logErrorStats(logger, true)
152
123
 
153
- errors++
124
+ await beforeFinal?.() // call beforeFinal if defined
154
125
 
155
- logErrorStats(logger)
126
+ if (collectedErrors.length) {
127
+ // emit Aggregated error
128
+ // For the same reason, magically, let's not call `cb`, but emit an error event instead
129
+ // this.emit('error', new AggregatedError(collectedErrors))
130
+ cb(new AggregatedError(collectedErrors))
131
+ } else {
132
+ // emit no error
133
+ // It is truly a mistery, but calling cb() here was causing ERR_MULTIPLE_CALLBACK ?!
134
+ // Commenting it out seems to work ?!
135
+ // ?!
136
+ // cb()
137
+ }
138
+ },
156
139
 
157
- if (onError) {
158
- try {
159
- onError(err, chunk)
160
- } catch {}
161
- }
140
+ async transform(this: Transform, chunk: IN, _encoding, cb) {
141
+ index++
142
+ // console.log('transform', {index})
162
143
 
163
- if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
164
- isRejected = true
165
- // Emit error immediately
166
- return cb(err)
167
- }
144
+ // Stop processing if THROW_IMMEDIATELY mode is used
145
+ if (isRejected && errorMode === ErrorMode.THROW_IMMEDIATELY) return cb()
168
146
 
169
- if (errorMode === ErrorMode.THROW_AGGREGATED) {
170
- collectedErrors.push(err as Error)
147
+ // It resolves when it is successfully STARTED execution.
148
+ // If it's queued instead - it'll wait and resolve only upon START.
149
+ await q.push(async () => {
150
+ try {
151
+ const currentIndex = index // because we need to pass it to 2 functions - mapper and predicate. Refers to INPUT index (since it may return multiple outputs)
152
+ const res = await mapper(chunk, currentIndex)
153
+ const passedResults = await pFilter(
154
+ flattenArrayOutput && Array.isArray(res) ? res : [res],
155
+ async r => await predicate(r, currentIndex),
156
+ )
157
+
158
+ passedResults.forEach(r => this.push(r))
159
+ } catch (err) {
160
+ logger.error(err)
161
+
162
+ errors++
163
+
164
+ logErrorStats(logger)
165
+
166
+ if (onError) {
167
+ try {
168
+ onError(err, chunk)
169
+ } catch {}
170
+ }
171
+
172
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
173
+ isRejected = true
174
+ // Emit error immediately
175
+ // return cb(err as Error)
176
+ return this.emit('error', err as Error)
177
+ }
178
+
179
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
180
+ collectedErrors.push(err as Error)
181
+ }
171
182
  }
183
+ })
172
184
 
173
- // Tell input stream that we're done processing, but emit nothing to output - not error nor result
174
- cb()
175
- }
185
+ // Resolved, which means it STARTED processing
186
+ // This means we can take more load
187
+ cb()
176
188
  },
177
- )
189
+ })
178
190
 
179
191
  function logErrorStats(logger: CommonLogger, final = false): void {
180
192
  if (!errors) return
@@ -1,5 +1,5 @@
1
1
  import { Transform } from 'stream'
2
- import { ErrorMode, Mapper } from '@naturalcycles/js-lib'
2
+ import { CommonLogger, ErrorMode, Mapper } from '@naturalcycles/js-lib'
3
3
  import { TransformTyped } from '../stream.model'
4
4
 
5
5
  export interface TransformMapSimpleOptions {
@@ -9,6 +9,8 @@ export interface TransformMapSimpleOptions {
9
9
  * @default ErrorMode.THROW_IMMEDIATELY
10
10
  */
11
11
  errorMode?: ErrorMode.THROW_IMMEDIATELY | ErrorMode.SUPPRESS
12
+
13
+ logger?: CommonLogger
12
14
  }
13
15
 
14
16
  /**
@@ -25,7 +27,7 @@ export function transformMapSimple<IN = any, OUT = IN>(
25
27
  opt: TransformMapSimpleOptions = {},
26
28
  ): TransformTyped<IN, OUT> {
27
29
  let index = -1
28
- const { errorMode = ErrorMode.THROW_IMMEDIATELY } = opt
30
+ const { errorMode = ErrorMode.THROW_IMMEDIATELY, logger = console } = opt
29
31
 
30
32
  return new Transform({
31
33
  objectMode: true,
@@ -33,7 +35,7 @@ export function transformMapSimple<IN = any, OUT = IN>(
33
35
  try {
34
36
  cb(null, mapper(chunk, ++index))
35
37
  } catch (err) {
36
- console.error(err)
38
+ logger.error(err)
37
39
 
38
40
  if (errorMode === ErrorMode.SUPPRESS) {
39
41
  cb() // suppress the error
@@ -1,5 +1,5 @@
1
1
  import { Transform } from 'stream'
2
- import { AggregatedError, ErrorMode, Mapper, Predicate } from '@naturalcycles/js-lib'
2
+ import { AggregatedError, CommonLogger, ErrorMode, Mapper, Predicate } from '@naturalcycles/js-lib'
3
3
  import { yellow } from '../../colors'
4
4
  import { TransformTyped } from '../stream.model'
5
5
  import { notNullishPredicate } from './transformMap'
@@ -43,6 +43,8 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
43
43
  * @default `stream`
44
44
  */
45
45
  metric?: string
46
+
47
+ logger?: CommonLogger
46
48
  }
47
49
 
48
50
  /**
@@ -62,6 +64,7 @@ export function transformMapSync<IN = any, OUT = IN>(
62
64
  onError,
63
65
  metric = 'stream',
64
66
  objectMode = true,
67
+ logger = console,
65
68
  } = opt
66
69
  let isRejected = false
67
70
  let errors = 0
@@ -92,7 +95,7 @@ export function transformMapSync<IN = any, OUT = IN>(
92
95
  cb(null, v)
93
96
  }
94
97
  } catch (err) {
95
- console.error(err)
98
+ logger.error(err)
96
99
  errors++
97
100
 
98
101
  logErrorStats()
@@ -134,6 +137,6 @@ export function transformMapSync<IN = any, OUT = IN>(
134
137
  function logErrorStats(final = false): void {
135
138
  if (!errors) return
136
139
 
137
- console.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`)
140
+ logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`)
138
141
  }
139
142
  }
@@ -1,7 +1,11 @@
1
1
  import { Transform } from 'stream'
2
- import { AsyncMapper } from '@naturalcycles/js-lib'
2
+ import { AsyncMapper, CommonLogger } from '@naturalcycles/js-lib'
3
3
  import { TransformOptions, TransformTyped } from '../stream.model'
4
4
 
5
+ export interface TransformTapOptions extends TransformOptions {
6
+ logger?: CommonLogger
7
+ }
8
+
5
9
  /**
6
10
  * Similar to RxJS `tap` - allows to run a function for each stream item, without affecting the result.
7
11
  * Item is passed through to the output.
@@ -10,8 +14,9 @@ import { TransformOptions, TransformTyped } from '../stream.model'
10
14
  */
11
15
  export function transformTap<IN>(
12
16
  fn: AsyncMapper<IN, any>,
13
- opt: TransformOptions = {},
17
+ opt: TransformTapOptions = {},
14
18
  ): TransformTyped<IN, IN> {
19
+ const { logger = console } = opt
15
20
  let index = 0
16
21
 
17
22
  return new Transform({
@@ -23,7 +28,7 @@ export function transformTap<IN>(
23
28
  try {
24
29
  await fn(chunk, index++)
25
30
  } catch (err) {
26
- console.error(err)
31
+ logger.error(err)
27
32
  // suppressed error
28
33
  }
29
34
 
@@ -1,5 +1,5 @@
1
- import { AsyncMapper, _passNothingPredicate } from '@naturalcycles/js-lib'
2
- import { transformMap, TransformMapOptions } from '../..'
1
+ import { AsyncMapper, _passNothingPredicate, Mapper } from '@naturalcycles/js-lib'
2
+ import { transformMap, TransformMapOptions, transformMapSync } from '../..'
3
3
  import { WritableTyped } from '../stream.model'
4
4
 
5
5
  /**
@@ -11,3 +11,13 @@ export function writableForEach<IN = any>(
11
11
  ): WritableTyped<IN> {
12
12
  return transformMap<IN, void>(mapper, { ...opt, predicate: _passNothingPredicate })
13
13
  }
14
+
15
+ /**
16
+ * Just an alias to transformMap that declares OUT as void.
17
+ */
18
+ export function writableForEachSync<IN = any>(
19
+ mapper: Mapper<IN, void>,
20
+ opt: TransformMapOptions<IN, void> = {},
21
+ ): WritableTyped<IN> {
22
+ return transformMapSync<IN, void>(mapper, { ...opt, predicate: _passNothingPredicate })
23
+ }
@@ -7,6 +7,8 @@ import { TransformOptions, WritableTyped } from '../stream.model'
7
7
  * Currently does NOT (!) maintain backpressure.
8
8
  * Error in the forked pipeline will propagate up to the main pipeline (and log error, to be sure).
9
9
  * Will wait until all forked pipelines are completed before completing the stream.
10
+ *
11
+ * @experimental
10
12
  */
11
13
  export function writableFork<T>(
12
14
  chains: NodeJS.WritableStream[][],
@@ -1,98 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.transformMap2 = exports.notNullishPredicate = void 0;
4
- const stream_1 = require("stream");
5
- const js_lib_1 = require("@naturalcycles/js-lib");
6
- const colors_1 = require("../../../colors");
7
- function notNullishPredicate(item) {
8
- return item !== undefined && item !== null;
9
- }
10
- exports.notNullishPredicate = notNullishPredicate;
11
- /**
12
- * Like pMap, but for streams.
13
- * Inspired by `through2`.
14
- * Main feature is concurrency control (implemented via `through2-concurrent`) and convenient options.
15
- * Using this allows native stream .pipe() to work and use backpressure.
16
- *
17
- * Only works in objectMode (due to through2Concurrent).
18
- *
19
- * Concurrency defaults to 16.
20
- *
21
- * If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
22
- */
23
- function transformMap2(mapper, opt = {}) {
24
- const { concurrency = 16, predicate = notNullishPredicate, errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput, onError, beforeFinal, metric = 'stream', logger = console, } = opt;
25
- let index = -1;
26
- let isRejected = false;
27
- let errors = 0;
28
- const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
29
- const q = new js_lib_1.PQueue({
30
- concurrency,
31
- resolveOn: 'start',
32
- // debug: true,
33
- });
34
- return new stream_1.Transform({
35
- objectMode: true,
36
- async final(cb) {
37
- // console.log('transformMap final', {index}, q.inFlight, q.queueSize)
38
- // wait for the current inFlight jobs to complete and push their results
39
- await q.onIdle();
40
- logErrorStats(logger, true);
41
- await beforeFinal?.(); // call beforeFinal if defined
42
- if (collectedErrors.length) {
43
- // emit Aggregated error
44
- cb(new js_lib_1.AggregatedError(collectedErrors));
45
- }
46
- else {
47
- // emit no error
48
- cb();
49
- }
50
- },
51
- async transform(chunk, _encoding, cb) {
52
- index++;
53
- // console.log('transform', {index})
54
- // Stop processing if THROW_IMMEDIATELY mode is used
55
- if (isRejected && errorMode === js_lib_1.ErrorMode.THROW_IMMEDIATELY)
56
- return cb();
57
- // It resolves when it is successfully STARTED execution.
58
- // If it's queued instead - it'll wait and resolve only upon START.
59
- await q.push(async () => {
60
- try {
61
- const currentIndex = index; // because we need to pass it to 2 functions - mapper and predicate. Refers to INPUT index (since it may return multiple outputs)
62
- const res = await mapper(chunk, currentIndex);
63
- const passedResults = await (0, js_lib_1.pFilter)(flattenArrayOutput && Array.isArray(res) ? res : [res], async (r) => await predicate(r, currentIndex));
64
- passedResults.forEach(r => this.push(r));
65
- }
66
- catch (err) {
67
- logger.error(err);
68
- errors++;
69
- logErrorStats(logger);
70
- if (onError) {
71
- try {
72
- onError(err, chunk);
73
- }
74
- catch { }
75
- }
76
- if (errorMode === js_lib_1.ErrorMode.THROW_IMMEDIATELY) {
77
- isRejected = true;
78
- // Emit error immediately
79
- // return cb(err as Error)
80
- return this.emit('error', err);
81
- }
82
- if (errorMode === js_lib_1.ErrorMode.THROW_AGGREGATED) {
83
- collectedErrors.push(err);
84
- }
85
- }
86
- });
87
- // Resolved, which means it STARTED processing
88
- // This means we can take more load
89
- cb();
90
- },
91
- });
92
- function logErrorStats(logger, final = false) {
93
- if (!errors)
94
- return;
95
- logger.log(`${metric} ${final ? 'final ' : ''}errors: ${(0, colors_1.yellow)(errors)}`);
96
- }
97
- }
98
- exports.transformMap2 = transformMap2;