@naturalcycles/nodejs-lib 15.20.2 → 15.22.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.
@@ -1,3 +1,4 @@
1
+ import { type StdioOptions } from 'node:child_process';
1
2
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js';
2
3
  import type { AnyObject, NumberOfMilliseconds } from '@naturalcycles/js-lib/types';
3
4
  /**
@@ -164,6 +165,10 @@ export interface SpawnOptions {
164
165
  * Set to false or true to override.
165
166
  */
166
167
  forceColor?: boolean;
168
+ /**
169
+ * Defaults to "inherit"
170
+ */
171
+ stdio?: StdioOptions;
167
172
  }
168
173
  export interface ExecOptions {
169
174
  /**
@@ -192,5 +197,10 @@ export interface ExecOptions {
192
197
  * Set to true to pass `process.env` to the spawned process.
193
198
  */
194
199
  passProcessEnv?: boolean;
200
+ /**
201
+ * Defaults to undefined.
202
+ * beware that stdio: 'inherit', means we don't get the output returned.
203
+ */
204
+ stdio?: StdioOptions;
195
205
  }
196
206
  export {};
@@ -40,7 +40,7 @@ class Exec2 {
40
40
  * log: true
41
41
  */
42
42
  spawn(cmd, opt = {}) {
43
- const { shell = true, cwd, env, passProcessEnv = true, forceColor = hasColors } = opt;
43
+ const { shell = true, cwd, env, passProcessEnv = true, forceColor = hasColors, stdio = 'inherit', } = opt;
44
44
  opt.log ??= true; // by default log should be true, as we are printing the output
45
45
  opt.logStart ??= opt.log;
46
46
  opt.logFinish ??= opt.log;
@@ -48,7 +48,7 @@ class Exec2 {
48
48
  this.logStart(cmd, opt);
49
49
  const r = spawnSync(cmd, opt.args, {
50
50
  encoding: 'utf8',
51
- stdio: 'inherit',
51
+ stdio,
52
52
  shell,
53
53
  cwd,
54
54
  env: {
@@ -81,7 +81,7 @@ class Exec2 {
81
81
  * log: false
82
82
  */
83
83
  exec(cmd, opt = {}) {
84
- const { cwd, env, passProcessEnv = true, timeout } = opt;
84
+ const { cwd, env, passProcessEnv = true, timeout, stdio } = opt;
85
85
  opt.logStart ??= opt.log ?? false;
86
86
  opt.logFinish ??= opt.log ?? false;
87
87
  const started = Date.now();
@@ -89,8 +89,7 @@ class Exec2 {
89
89
  try {
90
90
  const s = execSync(cmd, {
91
91
  encoding: 'utf8',
92
- // stdio: 'inherit', // no, otherwise we don't get the output returned
93
- stdio: undefined,
92
+ stdio,
94
93
  // shell: undefined,
95
94
  cwd,
96
95
  timeout,
@@ -7,6 +7,7 @@ export * from './ndjson/transformJsonParse.js';
7
7
  export * from './ndjson/transformToNDJson.js';
8
8
  export * from './pipeline/pipeline.js';
9
9
  export * from './progressLogger.js';
10
+ export * from './readable/readableCombined.js';
10
11
  export * from './readable/readableCreate.js';
11
12
  export * from './readable/readableForEach.js';
12
13
  export * from './readable/readableFromArray.js';
@@ -7,6 +7,7 @@ export * from './ndjson/transformJsonParse.js';
7
7
  export * from './ndjson/transformToNDJson.js';
8
8
  export * from './pipeline/pipeline.js';
9
9
  export * from './progressLogger.js';
10
+ export * from './readable/readableCombined.js';
10
11
  export * from './readable/readableCreate.js';
11
12
  export * from './readable/readableForEach.js';
12
13
  export * from './readable/readableFromArray.js';
@@ -19,7 +19,6 @@ export function transformJsonParse(opt = {}) {
19
19
  writableObjectMode: false,
20
20
  readableObjectMode: true,
21
21
  // highWatermark increased, because it's proven to be faster: https://github.com/nodejs/node/pull/52037
22
- // todo: it'll be default in Node 22, then we can remove this
23
22
  writableHighWaterMark: 64 * 1024,
24
23
  transform(chunk, _, cb) {
25
24
  try {
@@ -0,0 +1,30 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream';
3
+ /**
4
+ * Allows to combine multiple Readables into 1 Readable.
5
+ * As soon as any of the input Readables emit - the output Readable emits
6
+ * (passes through).
7
+ * Order is not preserved in any way, first come first served!
8
+ *
9
+ * Readable completes when all input Readables complete.
10
+ *
11
+ * @experimental
12
+ */
13
+ export declare class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
14
+ inputs: Readable[];
15
+ static create<T>(inputs: Readable[]): ReadableCombined<T>;
16
+ private constructor();
17
+ /**
18
+ * If defined - we are in Paused mode
19
+ * and should await the lock to be resolved before proceeding.
20
+ *
21
+ * If not defined - we are in Flowing mode, no limits in data flow.
22
+ */
23
+ private lock?;
24
+ private countIn;
25
+ private countOut;
26
+ private countReads;
27
+ private start;
28
+ _read(): void;
29
+ private logStats;
30
+ }
@@ -0,0 +1,77 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
3
+ import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
4
+ /**
5
+ * Allows to combine multiple Readables into 1 Readable.
6
+ * As soon as any of the input Readables emit - the output Readable emits
7
+ * (passes through).
8
+ * Order is not preserved in any way, first come first served!
9
+ *
10
+ * Readable completes when all input Readables complete.
11
+ *
12
+ * @experimental
13
+ */
14
+ export class ReadableCombined extends Readable {
15
+ inputs;
16
+ static create(inputs) {
17
+ return new ReadableCombined(inputs);
18
+ }
19
+ constructor(inputs) {
20
+ super({ objectMode: true });
21
+ this.inputs = inputs;
22
+ void this.start();
23
+ }
24
+ /**
25
+ * If defined - we are in Paused mode
26
+ * and should await the lock to be resolved before proceeding.
27
+ *
28
+ * If not defined - we are in Flowing mode, no limits in data flow.
29
+ */
30
+ lock;
31
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
32
+ countIn = 0;
33
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
34
+ countOut = 0;
35
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
36
+ countReads = 0;
37
+ async start() {
38
+ await pMap(this.inputs, async (input, i) => {
39
+ for await (const item of input) {
40
+ this.countIn++;
41
+ this.logStats();
42
+ if (this.lock) {
43
+ await this.lock;
44
+ // lock is undefined at this point
45
+ }
46
+ const shouldContinue = this.push(item);
47
+ this.countOut++;
48
+ if (!shouldContinue && !this.lock) {
49
+ this.lock = pDefer();
50
+ console.log(`ReadableCombined.push #${i} returned false, pausing the flow!`);
51
+ }
52
+ }
53
+ console.log(`ReadableCombined: input #${i} done`);
54
+ });
55
+ console.log(`ReadableCombined: all inputs done!`);
56
+ this.push(null);
57
+ }
58
+ _read() {
59
+ this.countReads++;
60
+ if (this.lock) {
61
+ console.log(`ReadableCombined._read: resuming the flow!`);
62
+ // calling it in this order is important!
63
+ // this.lock should be undefined BEFORE we call lock.resolve()
64
+ const { lock } = this;
65
+ this.lock = undefined;
66
+ lock.resolve();
67
+ }
68
+ }
69
+ logStats() {
70
+ const { countIn, countOut, countReads } = this;
71
+ console.log({
72
+ countIn,
73
+ countOut,
74
+ countReads,
75
+ });
76
+ }
77
+ }
@@ -1,6 +1,6 @@
1
1
  import { ErrorMode } from '@naturalcycles/js-lib/error';
2
2
  import type { CommonLogger } from '@naturalcycles/js-lib/log';
3
- import { type AbortableAsyncMapper, type AsyncPredicate, END, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
3
+ import { type AbortableAsyncMapper, type AsyncPredicate, END, type PositiveInteger, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
4
4
  import type { TransformTyped } from '../stream.model.js';
5
5
  export interface TransformMapOptions<IN = any, OUT = IN> {
6
6
  /**
@@ -21,7 +21,15 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
21
21
  * UPD: changed back from 32 to 16, "to be on a safe side", as 32 sometimes
22
22
  * causes "Datastore timeout errors".
23
23
  */
24
- concurrency?: number;
24
+ concurrency?: PositiveInteger;
25
+ /**
26
+ * Defaults to 64 items.
27
+ * (objectMode default is 16, but we increased it)
28
+ *
29
+ * Affects both readable and writable highWaterMark (buffer).
30
+ * So, 64 means a total buffer of 128 (64 input and 64 output buffer).
31
+ */
32
+ highWaterMark?: PositiveInteger;
25
33
  /**
26
34
  * @default THROW_IMMEDIATELY
27
35
  */
@@ -21,7 +21,7 @@ import { pipelineClose } from '../stream.util.js';
21
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
22
  */
23
23
  export function transformMap(mapper, opt = {}) {
24
- const { concurrency = 16, predicate, // we now default to "no predicate" (meaning pass-everything)
24
+ const { concurrency = 16, highWaterMark = 64, predicate, // we now default to "no predicate" (meaning pass-everything)
25
25
  errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', logger = console, } = opt;
26
26
  const started = Date.now();
27
27
  let index = -1;
@@ -31,6 +31,8 @@ export function transformMap(mapper, opt = {}) {
31
31
  const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
32
32
  return through2Concurrent.obj({
33
33
  maxConcurrency: concurrency,
34
+ readableHighWaterMark: highWaterMark,
35
+ writableHighWaterMark: highWaterMark,
34
36
  async final(cb) {
35
37
  // console.log('transformMap final')
36
38
  logErrorStats(true);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.20.2",
4
+ "version": "15.22.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -1,4 +1,4 @@
1
- import { execSync, spawn, spawnSync } from 'node:child_process'
1
+ import { execSync, spawn, spawnSync, type StdioOptions } from 'node:child_process'
2
2
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
3
3
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js'
4
4
  import { _substringAfterLast } from '@naturalcycles/js-lib/string/string.util.js'
@@ -46,7 +46,14 @@ class Exec2 {
46
46
  * log: true
47
47
  */
48
48
  spawn(cmd: string, opt: SpawnOptions = {}): void {
49
- const { shell = true, cwd, env, passProcessEnv = true, forceColor = hasColors } = opt
49
+ const {
50
+ shell = true,
51
+ cwd,
52
+ env,
53
+ passProcessEnv = true,
54
+ forceColor = hasColors,
55
+ stdio = 'inherit',
56
+ } = opt
50
57
  opt.log ??= true // by default log should be true, as we are printing the output
51
58
  opt.logStart ??= opt.log
52
59
  opt.logFinish ??= opt.log
@@ -55,7 +62,7 @@ class Exec2 {
55
62
 
56
63
  const r = spawnSync(cmd, opt.args, {
57
64
  encoding: 'utf8',
58
- stdio: 'inherit',
65
+ stdio,
59
66
  shell,
60
67
  cwd,
61
68
  env: {
@@ -91,7 +98,7 @@ class Exec2 {
91
98
  * log: false
92
99
  */
93
100
  exec(cmd: string, opt: ExecOptions = {}): string {
94
- const { cwd, env, passProcessEnv = true, timeout } = opt
101
+ const { cwd, env, passProcessEnv = true, timeout, stdio } = opt
95
102
  opt.logStart ??= opt.log ?? false
96
103
  opt.logFinish ??= opt.log ?? false
97
104
  const started = Date.now() as UnixTimestampMillis
@@ -100,8 +107,7 @@ class Exec2 {
100
107
  try {
101
108
  const s = execSync(cmd, {
102
109
  encoding: 'utf8',
103
- // stdio: 'inherit', // no, otherwise we don't get the output returned
104
- stdio: undefined,
110
+ stdio,
105
111
  // shell: undefined,
106
112
  cwd,
107
113
  timeout,
@@ -398,6 +404,11 @@ export interface SpawnOptions {
398
404
  * Set to false or true to override.
399
405
  */
400
406
  forceColor?: boolean
407
+
408
+ /**
409
+ * Defaults to "inherit"
410
+ */
411
+ stdio?: StdioOptions
401
412
  }
402
413
 
403
414
  export interface ExecOptions {
@@ -429,4 +440,10 @@ export interface ExecOptions {
429
440
  * Set to true to pass `process.env` to the spawned process.
430
441
  */
431
442
  passProcessEnv?: boolean
443
+
444
+ /**
445
+ * Defaults to undefined.
446
+ * beware that stdio: 'inherit', means we don't get the output returned.
447
+ */
448
+ stdio?: StdioOptions
432
449
  }
@@ -7,6 +7,7 @@ export * from './ndjson/transformJsonParse.js'
7
7
  export * from './ndjson/transformToNDJson.js'
8
8
  export * from './pipeline/pipeline.js'
9
9
  export * from './progressLogger.js'
10
+ export * from './readable/readableCombined.js'
10
11
  export * from './readable/readableCreate.js'
11
12
  export * from './readable/readableForEach.js'
12
13
  export * from './readable/readableFromArray.js'
@@ -36,7 +36,6 @@ export function transformJsonParse<ROW = any>(
36
36
  writableObjectMode: false,
37
37
  readableObjectMode: true,
38
38
  // highWatermark increased, because it's proven to be faster: https://github.com/nodejs/node/pull/52037
39
- // todo: it'll be default in Node 22, then we can remove this
40
39
  writableHighWaterMark: 64 * 1024,
41
40
  transform(chunk: string, _, cb) {
42
41
  try {
@@ -0,0 +1,87 @@
1
+ import { Readable } from 'node:stream'
2
+ import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
3
+ import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
4
+ import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream'
5
+
6
+ /**
7
+ * Allows to combine multiple Readables into 1 Readable.
8
+ * As soon as any of the input Readables emit - the output Readable emits
9
+ * (passes through).
10
+ * Order is not preserved in any way, first come first served!
11
+ *
12
+ * Readable completes when all input Readables complete.
13
+ *
14
+ * @experimental
15
+ */
16
+ export class ReadableCombined<T> extends Readable implements ReadableTyped<T> {
17
+ static create<T>(inputs: Readable[]): ReadableCombined<T> {
18
+ return new ReadableCombined<T>(inputs)
19
+ }
20
+
21
+ private constructor(public inputs: Readable[]) {
22
+ super({ objectMode: true })
23
+ void this.start()
24
+ }
25
+
26
+ /**
27
+ * If defined - we are in Paused mode
28
+ * and should await the lock to be resolved before proceeding.
29
+ *
30
+ * If not defined - we are in Flowing mode, no limits in data flow.
31
+ */
32
+ private lock?: DeferredPromise
33
+
34
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
35
+ private countIn = 0
36
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
37
+ private countOut = 0
38
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
39
+ private countReads = 0
40
+
41
+ private async start(): Promise<void> {
42
+ await pMap(this.inputs, async (input, i) => {
43
+ for await (const item of input) {
44
+ this.countIn++
45
+ this.logStats()
46
+ if (this.lock) {
47
+ await this.lock
48
+ // lock is undefined at this point
49
+ }
50
+
51
+ const shouldContinue = this.push(item)
52
+ this.countOut++
53
+ if (!shouldContinue && !this.lock) {
54
+ this.lock = pDefer()
55
+ console.log(`ReadableCombined.push #${i} returned false, pausing the flow!`)
56
+ }
57
+ }
58
+
59
+ console.log(`ReadableCombined: input #${i} done`)
60
+ })
61
+
62
+ console.log(`ReadableCombined: all inputs done!`)
63
+ this.push(null)
64
+ }
65
+
66
+ override _read(): void {
67
+ this.countReads++
68
+
69
+ if (this.lock) {
70
+ console.log(`ReadableCombined._read: resuming the flow!`)
71
+ // calling it in this order is important!
72
+ // this.lock should be undefined BEFORE we call lock.resolve()
73
+ const { lock } = this
74
+ this.lock = undefined
75
+ lock.resolve()
76
+ }
77
+ }
78
+
79
+ private logStats(): void {
80
+ const { countIn, countOut, countReads } = this
81
+ console.log({
82
+ countIn,
83
+ countOut,
84
+ countReads,
85
+ })
86
+ }
87
+ }
@@ -7,6 +7,7 @@ import {
7
7
  type AbortableAsyncMapper,
8
8
  type AsyncPredicate,
9
9
  END,
10
+ type PositiveInteger,
10
11
  type Promisable,
11
12
  SKIP,
12
13
  type StringMap,
@@ -38,7 +39,16 @@ export interface TransformMapOptions<IN = any, OUT = IN> {
38
39
  * UPD: changed back from 32 to 16, "to be on a safe side", as 32 sometimes
39
40
  * causes "Datastore timeout errors".
40
41
  */
41
- concurrency?: number
42
+ concurrency?: PositiveInteger
43
+
44
+ /**
45
+ * Defaults to 64 items.
46
+ * (objectMode default is 16, but we increased it)
47
+ *
48
+ * Affects both readable and writable highWaterMark (buffer).
49
+ * So, 64 means a total buffer of 128 (64 input and 64 output buffer).
50
+ */
51
+ highWaterMark?: PositiveInteger
42
52
 
43
53
  /**
44
54
  * @default THROW_IMMEDIATELY
@@ -123,6 +133,7 @@ export function transformMap<IN = any, OUT = IN>(
123
133
  ): TransformTyped<IN, OUT> {
124
134
  const {
125
135
  concurrency = 16,
136
+ highWaterMark = 64,
126
137
  predicate, // we now default to "no predicate" (meaning pass-everything)
127
138
  errorMode = ErrorMode.THROW_IMMEDIATELY,
128
139
  onError,
@@ -141,6 +152,8 @@ export function transformMap<IN = any, OUT = IN>(
141
152
  return through2Concurrent.obj(
142
153
  {
143
154
  maxConcurrency: concurrency,
155
+ readableHighWaterMark: highWaterMark,
156
+ writableHighWaterMark: highWaterMark,
144
157
  async final(cb) {
145
158
  // console.log('transformMap final')
146
159