@naturalcycles/nodejs-lib 15.26.0 → 15.27.1

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 (54) hide show
  1. package/dist/slack/slack.service.d.ts +1 -0
  2. package/dist/slack/slack.service.js +4 -3
  3. package/dist/stream/index.d.ts +2 -3
  4. package/dist/stream/index.js +2 -3
  5. package/dist/stream/ndjson/createReadStreamAsNDJson.d.ts +16 -0
  6. package/dist/stream/ndjson/createReadStreamAsNDJson.js +35 -0
  7. package/dist/stream/pipeline.d.ts +1 -0
  8. package/dist/stream/pipeline.js +12 -10
  9. package/dist/stream/progressLogger.d.ts +3 -3
  10. package/dist/stream/readable/readableCombined.d.ts +4 -2
  11. package/dist/stream/readable/readableCombined.js +16 -11
  12. package/dist/stream/readable/readableCreate.d.ts +1 -3
  13. package/dist/stream/readable/readableCreate.js +4 -4
  14. package/dist/stream/stream.model.d.ts +16 -0
  15. package/dist/stream/transform/transformFork.d.ts +10 -0
  16. package/dist/stream/transform/transformFork.js +62 -0
  17. package/dist/stream/transform/transformLimit.d.ts +2 -1
  18. package/dist/stream/transform/transformLimit.js +3 -3
  19. package/dist/stream/transform/transformLogProgress.js +3 -2
  20. package/dist/stream/transform/transformMap.d.ts +2 -4
  21. package/dist/stream/transform/transformMap.js +3 -2
  22. package/dist/stream/transform/transformMapSimple.d.ts +2 -4
  23. package/dist/stream/transform/transformMapSimple.js +3 -2
  24. package/dist/stream/transform/transformMapSync.d.ts +2 -4
  25. package/dist/stream/transform/transformMapSync.js +3 -1
  26. package/dist/stream/transform/transformSplit.js +2 -2
  27. package/dist/stream/transform/transformThrottle.d.ts +2 -3
  28. package/dist/stream/transform/transformThrottle.js +22 -27
  29. package/package.json +1 -1
  30. package/src/slack/slack.service.ts +6 -3
  31. package/src/stream/index.ts +2 -3
  32. package/src/stream/ndjson/createReadStreamAsNDJson.ts +43 -0
  33. package/src/stream/pipeline.ts +13 -12
  34. package/src/stream/progressLogger.ts +3 -3
  35. package/src/stream/readable/readableCombined.ts +22 -11
  36. package/src/stream/readable/readableCreate.ts +4 -3
  37. package/src/stream/stream.model.ts +18 -0
  38. package/src/stream/transform/transformFork.ts +74 -0
  39. package/src/stream/transform/transformLimit.ts +5 -4
  40. package/src/stream/transform/transformLogProgress.ts +3 -2
  41. package/src/stream/transform/transformMap.ts +4 -8
  42. package/src/stream/transform/transformMapSimple.ts +10 -7
  43. package/src/stream/transform/transformMapSync.ts +4 -6
  44. package/src/stream/transform/transformSplit.ts +2 -2
  45. package/src/stream/transform/transformThrottle.ts +28 -36
  46. package/dist/stream/transform/transformTee.d.ts +0 -13
  47. package/dist/stream/transform/transformTee.js +0 -37
  48. package/dist/stream/transform/transformToArray.d.ts +0 -5
  49. package/dist/stream/transform/transformToArray.js +0 -20
  50. package/dist/stream/writable/writableFork.d.ts +0 -10
  51. package/dist/stream/writable/writableFork.js +0 -45
  52. package/src/stream/transform/transformTee.ts +0 -48
  53. package/src/stream/transform/transformToArray.ts +0 -23
  54. package/src/stream/writable/writableFork.ts +0 -56
@@ -14,7 +14,7 @@ export function transformSplitOnNewline() {
14
14
  writableObjectMode: false,
15
15
  writableHighWaterMark: 64 * 1024,
16
16
  readableObjectMode: true,
17
- transform(buf, _enc, done) {
17
+ transform(buf, _enc, cb) {
18
18
  let offset = 0;
19
19
  let lastMatch = 0;
20
20
  if (buffered) {
@@ -36,7 +36,7 @@ export function transformSplitOnNewline() {
36
36
  break;
37
37
  }
38
38
  }
39
- done();
39
+ cb();
40
40
  },
41
41
  flush(done) {
42
42
  if (buffered && buffered.length > 0)
@@ -1,6 +1,6 @@
1
1
  import type { NumberOfSeconds, PositiveInteger } from '@naturalcycles/js-lib/types';
2
- import type { TransformTyped } from '../stream.model.js';
3
- export interface TransformThrottleOptions {
2
+ import type { TransformOptions, TransformTyped } from '../stream.model.js';
3
+ export interface TransformThrottleOptions extends TransformOptions {
4
4
  /**
5
5
  * How many items to allow per `interval` of seconds.
6
6
  */
@@ -9,7 +9,6 @@ export interface TransformThrottleOptions {
9
9
  * How long is the interval (in seconds) where number of items should not exceed `throughput`.
10
10
  */
11
11
  interval: NumberOfSeconds;
12
- debug?: boolean;
13
12
  }
14
13
  /**
15
14
  * Allows to throttle the throughput of the stream.
@@ -1,5 +1,6 @@
1
1
  import { Transform } from 'node:stream';
2
2
  import { _ms, _since, localTime } from '@naturalcycles/js-lib/datetime';
3
+ import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
3
4
  import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
4
5
  /**
5
6
  * Allows to throttle the throughput of the stream.
@@ -19,36 +20,34 @@ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
19
20
  * @experimental
20
21
  */
21
22
  export function transformThrottle(opt) {
22
- const { throughput, interval, debug } = opt;
23
+ const { throughput, interval, objectMode = true, highWaterMark } = opt;
23
24
  let count = 0;
24
25
  let start;
25
- let paused;
26
+ let lock;
26
27
  let timeout;
28
+ const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
27
29
  return new Transform({
28
- objectMode: true,
30
+ objectMode,
31
+ highWaterMark,
29
32
  async transform(item, _, cb) {
30
33
  // console.log('incoming', item, { paused: !!paused, count })
31
34
  if (!start) {
32
35
  start = Date.now();
33
36
  timeout = setTimeout(() => onInterval(this), interval * 1000);
34
- if (debug) {
35
- console.log(`${localTime.now().toPretty()} transformThrottle started with`, {
36
- throughput,
37
- interval,
38
- rps: Math.round(throughput / interval),
39
- });
40
- }
37
+ logger.log(`${localTime.now().toPretty()} transformThrottle started with`, {
38
+ throughput,
39
+ interval,
40
+ rps: Math.round(throughput / interval),
41
+ });
41
42
  }
42
- if (paused) {
43
- // console.log('awaiting pause', {item, count})
44
- await paused;
43
+ if (lock) {
44
+ // console.log('awaiting lock', {item, count})
45
+ await lock;
45
46
  }
46
47
  if (++count >= throughput) {
47
48
  // console.log('pausing now after', {item, count})
48
- paused = pDefer();
49
- if (debug) {
50
- console.log(`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (Date.now() - start))}`);
51
- }
49
+ lock = pDefer();
50
+ logger.log(`${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (Date.now() - start))}`);
52
51
  }
53
52
  cb(null, item); // pass the item through
54
53
  },
@@ -58,20 +57,16 @@ export function transformThrottle(opt) {
58
57
  },
59
58
  });
60
59
  function onInterval(transform) {
61
- if (paused) {
62
- if (debug) {
63
- console.log(`${localTime.now().toPretty()} transformThrottle resumed`);
64
- }
65
- paused.resolve();
66
- paused = undefined;
60
+ if (lock) {
61
+ logger.log(`${localTime.now().toPretty()} transformThrottle resumed`);
62
+ lock.resolve();
63
+ lock = undefined;
67
64
  }
68
65
  else {
69
- if (debug) {
70
- console.log(`${localTime.now().toPretty()} transformThrottle passed ${count} (of max ${throughput}) items in ${_since(start)}`);
71
- }
66
+ logger.log(`${localTime.now().toPretty()} transformThrottle passed ${count} (of max ${throughput}) items in ${_since(start)}`);
72
67
  }
73
68
  count = 0;
74
- start = Date.now();
69
+ start = localTime.nowUnixMillis();
75
70
  timeout = setTimeout(() => onInterval(transform), interval * 1000);
76
71
  }
77
72
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.26.0",
4
+ "version": "15.27.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -2,8 +2,8 @@ import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
2
2
  import { type Fetcher, getFetcher } from '@naturalcycles/js-lib/http'
3
3
  import {
4
4
  type CommonLogger,
5
- commonLoggerMinLevel,
6
5
  type CommonLogLevel,
6
+ createCommonLoggerAtLevel,
7
7
  } from '@naturalcycles/js-lib/log'
8
8
  import { _omit } from '@naturalcycles/js-lib/object/object.util.js'
9
9
  import { PQueue } from '@naturalcycles/js-lib/promise/pQueue.js'
@@ -152,19 +152,22 @@ export class SlackService<CTX = any> {
152
152
  */
153
153
  getCommonLogger(opt: {
154
154
  minLogLevel: CommonLogLevel
155
+ debugChannel?: string
155
156
  logChannel?: string
156
157
  warnChannel?: string
157
158
  errorChannel?: string
158
159
  }): CommonLogger {
159
- const { minLogLevel = 'log', logChannel, warnChannel, errorChannel } = opt
160
+ const { minLogLevel = 'log', debugChannel, logChannel, warnChannel, errorChannel } = opt
160
161
  const defaultChannel = this.cfg.defaults?.channel || DEFAULTS.channel!
161
162
 
162
163
  const q = new PQueue({
163
164
  concurrency: 1,
164
165
  })
165
166
 
166
- return commonLoggerMinLevel(
167
+ return createCommonLoggerAtLevel(
167
168
  {
169
+ debug: (...args) =>
170
+ q.push(() => this.send({ items: args, channel: debugChannel || defaultChannel })),
168
171
  log: (...args) =>
169
172
  q.push(() => this.send({ items: args, channel: logChannel || defaultChannel })),
170
173
  warn: (...args) =>
@@ -1,3 +1,4 @@
1
+ export * from './ndjson/createReadStreamAsNDJson.js'
1
2
  export * from './ndjson/ndjson.model.js'
2
3
  export * from './ndjson/ndjsonMap.js'
3
4
  export * from './ndjson/transformJsonParse.js'
@@ -11,6 +12,7 @@ export * from './stream.model.js'
11
12
  export * from './transform/transformChunk.js'
12
13
  export * from './transform/transformFilter.js'
13
14
  export * from './transform/transformFlatten.js'
15
+ export * from './transform/transformFork.js'
14
16
  export * from './transform/transformLimit.js'
15
17
  export * from './transform/transformLogProgress.js'
16
18
  export * from './transform/transformMap.js'
@@ -20,12 +22,9 @@ export * from './transform/transformNoOp.js'
20
22
  export * from './transform/transformOffset.js'
21
23
  export * from './transform/transformSplit.js'
22
24
  export * from './transform/transformTap.js'
23
- export * from './transform/transformTee.js'
24
25
  export * from './transform/transformThrottle.js'
25
- export * from './transform/transformToArray.js'
26
26
  export * from './transform/worker/baseWorkerClass.js'
27
27
  export * from './transform/worker/transformMultiThreaded.js'
28
28
  export * from './transform/worker/transformMultiThreaded.model.js'
29
- export * from './writable/writableFork.js'
30
29
  export * from './writable/writablePushToArray.js'
31
30
  export * from './writable/writableVoid.js'
@@ -0,0 +1,43 @@
1
+ import { createUnzip } from 'node:zlib'
2
+ import { fs2 } from '../../fs/fs2.js'
3
+ import type { ReadableTyped } from '../stream.model.js'
4
+ import { transformSplitOnNewline } from '../transform/transformSplit.js'
5
+
6
+ /**
7
+ Returns a Readable of [already parsed] NDJSON objects.
8
+
9
+ Replaces a list of operations:
10
+ - requireFileToExist(inputPath)
11
+ - fs.createReadStream
12
+ - createUnzip (only if path ends with '.gz')
13
+ - transformSplitOnNewline
14
+ - transformJsonParse
15
+
16
+ To add a Limit or Offset: just add .take() or .drop(), example:
17
+
18
+ createReadStreamAsNDJson().take(100)
19
+ */
20
+
21
+ export function createReadStreamAsNDJson<ROW = any>(inputPath: string): ReadableTyped<ROW> {
22
+ fs2.requireFileToExist(inputPath)
23
+
24
+ let stream: ReadableTyped<ROW> = fs2
25
+ .createReadStream(inputPath, {
26
+ highWaterMark: 64 * 1024, // no observed speedup
27
+ })
28
+ .on('error', err => stream.emit('error', err))
29
+
30
+ if (inputPath.endsWith('.gz')) {
31
+ stream = stream.pipe(
32
+ createUnzip({
33
+ chunkSize: 64 * 1024, // speedup from ~3200 to 3800 rps!
34
+ }),
35
+ )
36
+ }
37
+
38
+ return stream.pipe(transformSplitOnNewline()).map(line => JSON.parse(line))
39
+ // For some crazy reason .map is much faster than transformJsonParse!
40
+ // ~5000 vs ~4000 rps !!!
41
+ // .on('error', err => stream.emit('error', err))
42
+ // .pipe(transformJsonParse<ROW>())
43
+ }
@@ -17,6 +17,7 @@ import type {
17
17
  SKIP,
18
18
  } from '@naturalcycles/js-lib/types'
19
19
  import { fs2 } from '../fs/fs2.js'
20
+ import { createReadStreamAsNDJson } from './ndjson/createReadStreamAsNDJson.js'
20
21
  import { transformJsonParse } from './ndjson/transformJsonParse.js'
21
22
  import { transformToNDJson } from './ndjson/transformToNDJson.js'
22
23
  import type {
@@ -29,6 +30,7 @@ import { PIPELINE_GRACEFUL_ABORT } from './stream.util.js'
29
30
  import { transformChunk } from './transform/transformChunk.js'
30
31
  import { transformFilterSync } from './transform/transformFilter.js'
31
32
  import { transformFlatten, transformFlattenIfNeeded } from './transform/transformFlatten.js'
33
+ import { transformFork } from './transform/transformFork.js'
32
34
  import { transformLimit } from './transform/transformLimit.js'
33
35
  import {
34
36
  transformLogProgress,
@@ -83,16 +85,12 @@ export class Pipeline<T> {
83
85
  }
84
86
 
85
87
  static fromNDJsonFile<T>(sourceFilePath: string): Pipeline<T> {
86
- fs2.requireFileToExist(sourceFilePath)
87
-
88
- const p = Pipeline.fromFile(sourceFilePath)
89
- if (sourceFilePath.endsWith('.gz')) {
90
- p.gunzip()
91
- }
92
- return p.parseJson()
93
- // return stream.pipe(transformSplitOnNewline()).map(line => JSON.parse(line))
94
- // For some crazy reason .map is much faster than transformJsonParse!
95
- // ~5000 vs ~4000 rps !!!
88
+ // Important that createReadStreamAsNDJson function is used
89
+ // (and not Pipeline set of individual transforms),
90
+ // because createReadStreamAsNDJson returns a Readable,
91
+ // hence it allows to apply .take(limit) on it
92
+ // e.g like Pipeline.fromNDJsonFile().limitSource(limit)
93
+ return new Pipeline<T>(createReadStreamAsNDJson(sourceFilePath))
96
94
  }
97
95
 
98
96
  static fromFile(sourceFilePath: string): Pipeline<Uint8Array> {
@@ -223,8 +221,6 @@ export class Pipeline<T> {
223
221
  return this
224
222
  }
225
223
 
226
- // todo: tee/fork
227
-
228
224
  transform<TO>(transform: TransformTyped<T, TO>): Pipeline<TO> {
229
225
  this.transforms.push(transform)
230
226
  return this as any
@@ -239,6 +235,11 @@ export class Pipeline<T> {
239
235
  return this as any
240
236
  }
241
237
 
238
+ fork<FORK>(fn: (pipeline: Pipeline<T>) => Pipeline<FORK>, opt?: TransformOptions): this {
239
+ this.transforms.push(transformFork(fn, opt))
240
+ return this
241
+ }
242
+
242
243
  /**
243
244
  * Utility method just to conveniently type-cast the current Pipeline type.
244
245
  * No runtime effect.
@@ -4,7 +4,7 @@ import { _hc, _mb } from '@naturalcycles/js-lib'
4
4
  import { _since, localTime } from '@naturalcycles/js-lib/datetime'
5
5
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
6
6
  import { SimpleMovingAverage } from '@naturalcycles/js-lib/math'
7
- import type { AnyObject, UnixTimestampMillis } from '@naturalcycles/js-lib/types'
7
+ import type { AnyObject, PositiveInteger, UnixTimestampMillis } from '@naturalcycles/js-lib/types'
8
8
  import { boldWhite, dimGrey, hasColors, white, yellow } from '../colors/colors.js'
9
9
  import { SizeStack } from './sizeStack.js'
10
10
  import type { ReadableMapper } from './stream.model.js'
@@ -87,7 +87,7 @@ export interface ProgressLoggerCfg<T = any> {
87
87
  *
88
88
  * @default 1000
89
89
  */
90
- logEvery?: number
90
+ logEvery?: PositiveInteger
91
91
 
92
92
  logger?: CommonLogger
93
93
 
@@ -111,7 +111,7 @@ export interface ProgressLoggerCfg<T = any> {
111
111
  *
112
112
  * Defaults to 1.
113
113
  */
114
- chunkSize?: number
114
+ chunkSize?: PositiveInteger
115
115
 
116
116
  /**
117
117
  * Experimental logging of item (shunk) sizes, when json-stringified.
@@ -1,7 +1,9 @@
1
1
  import { Readable } from 'node:stream'
2
+ import { type CommonLogger, createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
2
3
  import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
3
4
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
4
5
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
6
+ import type { TransformOptions } from '../stream.model.js'
5
7
 
6
8
  /**
7
9
  * Allows to combine multiple Readables into 1 Readable.
@@ -14,15 +16,22 @@ import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
14
16
  * @experimental
15
17
  */
16
18
  export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
17
- static create<T>(inputs: Readable[]): ReadableCombined<T> {
18
- return new ReadableCombined<T>(inputs)
19
+ static create<T>(inputs: Readable[], opt: TransformOptions = {}): ReadableCombined<T> {
20
+ return new ReadableCombined<T>(inputs, opt)
19
21
  }
20
22
 
21
- private constructor(public inputs: Readable[]) {
22
- super({ objectMode: true })
23
- void this.start()
23
+ private constructor(
24
+ public inputs: Readable[],
25
+ opt: TransformOptions,
26
+ ) {
27
+ const { objectMode = true, highWaterMark } = opt
28
+ super({ objectMode, highWaterMark })
29
+ this.logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
30
+ void this.run()
24
31
  }
25
32
 
33
+ private logger: CommonLogger
34
+
26
35
  /**
27
36
  * If defined - we are in Paused mode
28
37
  * and should await the lock to be resolved before proceeding.
@@ -38,7 +47,9 @@ export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
38
47
  // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
39
48
  private countReads = 0
40
49
 
41
- private async start(): Promise<void> {
50
+ private async run(): Promise<void> {
51
+ const { logger } = this
52
+
42
53
  await pMap(this.inputs, async (input, i) => {
43
54
  for await (const item of input) {
44
55
  this.countIn++
@@ -52,14 +63,14 @@ export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
52
63
  this.countOut++
53
64
  if (!shouldContinue && !this.lock) {
54
65
  this.lock = pDefer()
55
- console.log(`ReadableCombined.push #${i} returned false, pausing the flow!`)
66
+ logger.log(`ReadableCombined.push #${i} returned false, pausing the flow!`)
56
67
  }
57
68
  }
58
69
 
59
- console.log(`ReadableCombined: input #${i} done`)
70
+ logger.log(`ReadableCombined: input #${i} done`)
60
71
  })
61
72
 
62
- console.log(`ReadableCombined: all inputs done!`)
73
+ logger.log(`ReadableCombined: all inputs done!`)
63
74
  this.push(null)
64
75
  }
65
76
 
@@ -67,7 +78,7 @@ export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
67
78
  this.countReads++
68
79
 
69
80
  if (this.lock) {
70
- console.log(`ReadableCombined._read: resuming the flow!`)
81
+ this.logger.log(`ReadableCombined._read: resuming the flow!`)
71
82
  // calling it in this order is important!
72
83
  // this.lock should be undefined BEFORE we call lock.resolve()
73
84
  const { lock } = this
@@ -78,7 +89,7 @@ export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
78
89
 
79
90
  private logStats(): void {
80
91
  const { countIn, countOut, countReads } = this
81
- console.log({
92
+ this.logger.debug({
82
93
  countIn,
83
94
  countOut,
84
95
  countReads,
@@ -13,17 +13,18 @@ import type { ReadableTyped } from '../stream.model.js'
13
13
  * e.g the read() method doesn't return anything, so, it will hang the Node process (or cause it to process.exit(0))
14
14
  * if read() will be called AFTER everything was pushed and Readable is closed (by pushing `null`).
15
15
  * Beware of it when e.g doing unit testing! Jest prefers to hang (not exit-0).
16
- *
17
- * @deprecated because of the caution above
18
16
  */
19
17
  export function readableCreate<T>(
20
18
  items: Iterable<T> = [],
21
19
  opt?: ReadableOptions,
20
+ onRead?: () => void, // read callback
22
21
  ): ReadableTyped<T> {
23
22
  const readable = new Readable({
24
23
  objectMode: true,
25
24
  ...opt,
26
- read() {}, // Caution, if this is called and Readable has not finished yet (null wasn't pushed) - it'll hang the process!
25
+ read() {
26
+ onRead?.()
27
+ },
27
28
  })
28
29
  for (const item of items) {
29
30
  readable.push(item)
@@ -1,4 +1,5 @@
1
1
  import type { Readable, Transform, Writable } from 'node:stream'
2
+ import type { CommonLogger, CommonLogLevel } from '@naturalcycles/js-lib/log'
2
3
  import type { Promisable } from '@naturalcycles/js-lib/types'
3
4
 
4
5
  export interface ReadableSignalOptions {
@@ -72,4 +73,21 @@ export interface TransformOptions {
72
73
  * @default 16
73
74
  */
74
75
  highWaterMark?: number
76
+
77
+ /**
78
+ * Defaults to `console`.
79
+ */
80
+ logger?: CommonLogger
81
+
82
+ /**
83
+ * Not every Transform implements it.
84
+ * Can be one of:
85
+ * debug - most verbose, when debugging is needed
86
+ * log - default level
87
+ * error - logs errors and warnings only
88
+ *
89
+ * Default is 'log'.
90
+ *
91
+ */
92
+ logLevel?: CommonLogLevel
75
93
  }
@@ -0,0 +1,74 @@
1
+ import { Transform } from 'node:stream'
2
+ import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
3
+ import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
4
+ import { Pipeline } from '../pipeline.js'
5
+ import { readableCreate } from '../readable/readableCreate.js'
6
+ import type { TransformOptions, TransformTyped } from '../stream.model.js'
7
+
8
+ /**
9
+ * Allows to "fork" away from the "main pipeline" into the "forked pipeline".
10
+ *
11
+ * Correctly keeps backpressure from both "downstreams" (main and forked).
12
+ *
13
+ * @experimental
14
+ */
15
+ export function transformFork<T, FORK>(
16
+ fn: (pipeline: Pipeline<T>) => Pipeline<FORK>,
17
+ opt: TransformOptions = {},
18
+ ): TransformTyped<T, T> {
19
+ const { objectMode = true, highWaterMark } = opt
20
+ const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
21
+
22
+ let lock: DeferredPromise | undefined
23
+
24
+ const fork = readableCreate<T>([], {}, () => {
25
+ // `_read` is called
26
+ if (!lock) return
27
+ // We had a lock - let's Resume
28
+ logger.log(`TransformFork: resume`)
29
+ const lockCopy = lock
30
+ lock = undefined
31
+ lockCopy.resolve()
32
+ })
33
+
34
+ const p = fn(Pipeline.from<T>(fork))
35
+ void p.run().then(() => {
36
+ logger.log('TransformFork: done')
37
+ })
38
+
39
+ return new Transform({
40
+ objectMode,
41
+ highWaterMark,
42
+ async transform(chunk: T, _, cb) {
43
+ // pass through to the "main" pipeline
44
+ // Main pipeline should handle backpressure "automatically",
45
+ // so, we're not maintaining a Lock for it
46
+ this.push(chunk)
47
+
48
+ if (lock) {
49
+ // Forked pipeline is locked - let's wait for it to call _read
50
+ await lock
51
+ // lock is undefined at this point
52
+ }
53
+ // pass to the "forked" pipeline
54
+ const shouldContinue = fork.push(chunk)
55
+ if (!shouldContinue && !lock) {
56
+ // Forked pipeline indicates that we should Pause
57
+ lock = pDefer()
58
+ logger.log(`TransformFork: pause`)
59
+ }
60
+
61
+ // acknowledge that we've finished processing the input chunk
62
+ cb()
63
+ },
64
+ async final(cb) {
65
+ logger.log('TransformFork: final')
66
+
67
+ // Pushing null "closes"/ends the secondary pipeline correctly
68
+ fork.push(null)
69
+
70
+ // Acknowledge that we've received `null` and passed it through to the fork
71
+ cb()
72
+ },
73
+ })
74
+ }
@@ -1,5 +1,6 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import type { AbortableSignal } from '@naturalcycles/js-lib'
3
+ import type { NonNegativeInteger } from '@naturalcycles/js-lib/types'
3
4
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
4
5
  import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js'
5
6
  import { transformNoOp } from './transformNoOp.js'
@@ -8,7 +9,7 @@ export interface TransformLimitOptions extends TransformOptions {
8
9
  /**
9
10
  * Nullish value (e.g 0 or undefined) would mean "no limit"
10
11
  */
11
- limit?: number
12
+ limit?: NonNegativeInteger
12
13
 
13
14
  /**
14
15
  * Allows to abort (gracefully stop) the stream from inside the Transform.
@@ -17,7 +18,7 @@ export interface TransformLimitOptions extends TransformOptions {
17
18
  }
18
19
 
19
20
  export function transformLimit<IN>(opt: TransformLimitOptions): TransformTyped<IN, IN> {
20
- const { limit, signal } = opt
21
+ const { limit, signal, objectMode = true, highWaterMark } = opt
21
22
 
22
23
  if (!limit) {
23
24
  return transformNoOp()
@@ -26,8 +27,8 @@ export function transformLimit<IN>(opt: TransformLimitOptions): TransformTyped<I
26
27
  let i = 0 // so we start first chunk with 1
27
28
  let ended = false
28
29
  return new Transform({
29
- objectMode: true,
30
- ...opt,
30
+ objectMode,
31
+ highWaterMark,
31
32
  transform(chunk, _, cb) {
32
33
  if (ended) {
33
34
  return
@@ -13,11 +13,12 @@ export interface TransformLogProgressOptions<IN = any>
13
13
  export function transformLogProgress<IN = any>(
14
14
  opt: TransformLogProgressOptions = {},
15
15
  ): TransformTyped<IN, IN> {
16
+ const { objectMode = true, highWaterMark } = opt
16
17
  const progress = progressLogger(opt)
17
18
 
18
19
  return new Transform({
19
- objectMode: true,
20
- ...opt,
20
+ objectMode,
21
+ highWaterMark,
21
22
  transform(chunk: IN, _, cb) {
22
23
  progress.log(chunk)
23
24
  cb(null, chunk) // pass-through
@@ -1,7 +1,7 @@
1
1
  import { _hc, type AbortableSignal } from '@naturalcycles/js-lib'
2
2
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
3
3
  import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
4
- import type { CommonLogger } from '@naturalcycles/js-lib/log'
4
+ import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
5
5
  import { _stringify } from '@naturalcycles/js-lib/string/stringify.js'
6
6
  import {
7
7
  type AbortableAsyncMapper,
@@ -15,10 +15,10 @@ import {
15
15
  } from '@naturalcycles/js-lib/types'
16
16
  import through2Concurrent from 'through2-concurrent'
17
17
  import { yellow } from '../../colors/colors.js'
18
- import type { TransformTyped } from '../stream.model.js'
18
+ import type { TransformOptions, TransformTyped } from '../stream.model.js'
19
19
  import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js'
20
20
 
21
- export interface TransformMapOptions<IN = any, OUT = IN> {
21
+ export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOptions {
22
22
  /**
23
23
  * Predicate to filter outgoing results (after mapper).
24
24
  * Allows to not emit all results.
@@ -79,8 +79,6 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
79
79
  */
80
80
  metric?: string
81
81
 
82
- logger?: CommonLogger
83
-
84
82
  /**
85
83
  * Allows to abort (gracefully stop) the stream from inside the Transform.
86
84
  */
@@ -143,7 +141,6 @@ export function transformMap<IN = any, OUT = IN>(
143
141
  onError,
144
142
  onDone,
145
143
  metric = 'stream',
146
- logger = console,
147
144
  signal,
148
145
  } = opt
149
146
 
@@ -154,6 +151,7 @@ export function transformMap<IN = any, OUT = IN>(
154
151
  let ok = true
155
152
  let errors = 0
156
153
  const collectedErrors: Error[] = [] // only used if errorMode == THROW_AGGREGATED
154
+ const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
157
155
 
158
156
  return through2Concurrent.obj(
159
157
  {
@@ -161,8 +159,6 @@ export function transformMap<IN = any, OUT = IN>(
161
159
  readableHighWaterMark: highWaterMark,
162
160
  writableHighWaterMark: highWaterMark,
163
161
  async final(cb) {
164
- // console.log('transformMap final')
165
-
166
162
  logErrorStats(true)
167
163
 
168
164
  if (collectedErrors.length) {