@naturalcycles/nodejs-lib 15.27.3 → 15.29.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.
@@ -5,8 +5,8 @@ export * from './ndjson/transformJsonParse.js';
5
5
  export * from './ndjson/transformToNDJson.js';
6
6
  export * from './pipeline.js';
7
7
  export * from './progressLogger.js';
8
+ export * from './readable/createReadable.js';
8
9
  export * from './readable/readableCombined.js';
9
- export * from './readable/readableCreate.js';
10
10
  export * from './readable/readableFromArray.js';
11
11
  export * from './stream.model.js';
12
12
  export * from './transform/transformChunk.js';
@@ -5,8 +5,8 @@ export * from './ndjson/transformJsonParse.js';
5
5
  export * from './ndjson/transformToNDJson.js';
6
6
  export * from './pipeline.js';
7
7
  export * from './progressLogger.js';
8
+ export * from './readable/createReadable.js';
8
9
  export * from './readable/readableCombined.js';
9
- export * from './readable/readableCreate.js';
10
10
  export * from './readable/readableFromArray.js';
11
11
  export * from './stream.model.js';
12
12
  export * from './transform/transformChunk.js';
@@ -1,16 +1,15 @@
1
1
  import { type Transform } from 'node:stream';
2
2
  import type { ReadableStream as WebReadableStream } from 'node:stream/web';
3
3
  import { type ZlibOptions } from 'node:zlib';
4
- import type { AbortableAsyncMapper, AsyncIndexedMapper, AsyncPredicate, END, IndexedMapper, NonNegativeInteger, PositiveInteger, Predicate, SKIP } from '@naturalcycles/js-lib/types';
4
+ import { type AbortableAsyncMapper, type AsyncIndexedMapper, type AsyncPredicate, type END, type IndexedMapper, type NonNegativeInteger, type PositiveInteger, type Predicate, type SKIP } from '@naturalcycles/js-lib/types';
5
5
  import type { ReadableTyped, TransformOptions, TransformTyped, WritableTyped } from './stream.model.js';
6
6
  import { type TransformLogProgressOptions } from './transform/transformLogProgress.js';
7
7
  import { type TransformMapOptions } from './transform/transformMap.js';
8
8
  import { type TransformMapSimpleOptions } from './transform/transformMapSimple.js';
9
9
  import { type TransformMapSyncOptions } from './transform/transformMapSync.js';
10
10
  import { type TransformOffsetOptions } from './transform/transformOffset.js';
11
- import { type TransformTapOptions } from './transform/transformTap.js';
12
11
  import { type TransformThrottleOptions } from './transform/transformThrottle.js';
13
- export declare class Pipeline<T> {
12
+ export declare class Pipeline<T = unknown> {
14
13
  private readonly source;
15
14
  private transforms;
16
15
  private destination?;
@@ -19,6 +18,12 @@ export declare class Pipeline<T> {
19
18
  private abortableSignal;
20
19
  private constructor();
21
20
  static from<T>(source: ReadableTyped<T>): Pipeline<T>;
21
+ /**
22
+ * Useful in cases when Readable is not immediately available,
23
+ * but only available after an async operation is completed.
24
+ * Implemented via a proxy Transform, which should be transparent.
25
+ */
26
+ static fromAsyncReadable<T = unknown>(fn: () => Promise<ReadableTyped<T>>): Pipeline<T>;
22
27
  static fromWeb<T>(webReadableStream: WebReadableStream<T>): Pipeline<T>;
23
28
  /**
24
29
  * Technically same as `fromIterable` (since Array is Iterable),
@@ -58,10 +63,11 @@ export declare class Pipeline<T> {
58
63
  map<TO>(mapper: AbortableAsyncMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMapOptions<T, TO>): Pipeline<TO>;
59
64
  mapSync<TO>(mapper: IndexedMapper<T, TO | typeof SKIP | typeof END>, opt?: TransformMapSyncOptions): Pipeline<TO>;
60
65
  mapSimple<TO>(mapper: IndexedMapper<T, TO>, opt?: TransformMapSimpleOptions): Pipeline<TO>;
61
- filter(predicate: AsyncPredicate<T>, opt?: TransformMapOptions): this;
66
+ filter(asyncPredicate: AsyncPredicate<T>, opt?: TransformMapOptions): this;
62
67
  filterSync(predicate: Predicate<T>, opt?: TransformOptions): this;
63
68
  offset(opt: TransformOffsetOptions): this;
64
- tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformTapOptions): this;
69
+ tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformOptions): this;
70
+ tapSync(fn: IndexedMapper<T, any>, opt?: TransformOptions): this;
65
71
  throttle(opt: TransformThrottleOptions): this;
66
72
  transform<TO>(transform: TransformTyped<T, TO>): Pipeline<TO>;
67
73
  /**
@@ -90,7 +96,7 @@ export declare class Pipeline<T> {
90
96
  toFile(outputFilePath: string): Promise<void>;
91
97
  toNDJsonFile(outputFilePath: string): Promise<void>;
92
98
  to(destination: WritableTyped<T>): Promise<void>;
93
- forEach(fn: AsyncIndexedMapper<T, void>, opt?: TransformMapOptions<T, void>): Promise<void>;
94
- forEachSync(fn: IndexedMapper<T, void>, opt?: TransformMapSyncOptions<T, void>): Promise<void>;
99
+ forEach(fn: AsyncIndexedMapper<T, void>, opt?: TransformMapOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
100
+ forEachSync(fn: IndexedMapper<T, void>, opt?: TransformMapSyncOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
95
101
  run(): Promise<void>;
96
102
  }
@@ -3,10 +3,12 @@ import { pipeline } from 'node:stream/promises';
3
3
  import { createUnzip } from 'node:zlib';
4
4
  import { createGzip } from 'node:zlib';
5
5
  import { createAbortableSignal } from '@naturalcycles/js-lib';
6
+ import { _passthroughPredicate, } from '@naturalcycles/js-lib/types';
6
7
  import { fs2 } from '../fs/fs2.js';
7
8
  import { createReadStreamAsNDJson } from './ndjson/createReadStreamAsNDJson.js';
8
9
  import { transformJsonParse } from './ndjson/transformJsonParse.js';
9
10
  import { transformToNDJson } from './ndjson/transformToNDJson.js';
11
+ import { createReadableFromAsync } from './readable/createReadable.js';
10
12
  import { PIPELINE_GRACEFUL_ABORT } from './stream.util.js';
11
13
  import { transformChunk } from './transform/transformChunk.js';
12
14
  import { transformFilterSync } from './transform/transformFilter.js';
@@ -19,7 +21,7 @@ import { transformMapSimple, } from './transform/transformMapSimple.js';
19
21
  import { transformMapSync } from './transform/transformMapSync.js';
20
22
  import { transformOffset } from './transform/transformOffset.js';
21
23
  import { transformSplitOnNewline } from './transform/transformSplit.js';
22
- import { transformTap } from './transform/transformTap.js';
24
+ import { transformTap, transformTapSync } from './transform/transformTap.js';
23
25
  import { transformThrottle } from './transform/transformThrottle.js';
24
26
  import { writablePushToArray } from './writable/writablePushToArray.js';
25
27
  import { writableVoid } from './writable/writableVoid.js';
@@ -39,6 +41,14 @@ export class Pipeline {
39
41
  static from(source) {
40
42
  return new Pipeline(source);
41
43
  }
44
+ /**
45
+ * Useful in cases when Readable is not immediately available,
46
+ * but only available after an async operation is completed.
47
+ * Implemented via a proxy Transform, which should be transparent.
48
+ */
49
+ static fromAsyncReadable(fn) {
50
+ return new Pipeline(createReadableFromAsync(fn));
51
+ }
42
52
  static fromWeb(webReadableStream) {
43
53
  return new Pipeline(Readable.fromWeb(webReadableStream));
44
54
  }
@@ -132,9 +142,9 @@ export class Pipeline {
132
142
  this.transforms.push(transformMapSimple(mapper, opt));
133
143
  return this;
134
144
  }
135
- filter(predicate, opt) {
145
+ filter(asyncPredicate, opt) {
136
146
  this.transforms.push(transformMap(v => v, {
137
- predicate,
147
+ asyncPredicate,
138
148
  ...opt,
139
149
  signal: this.abortableSignal,
140
150
  }));
@@ -152,6 +162,10 @@ export class Pipeline {
152
162
  this.transforms.push(transformTap(fn, opt));
153
163
  return this;
154
164
  }
165
+ tapSync(fn, opt) {
166
+ this.transforms.push(transformTapSync(fn, opt));
167
+ return this;
168
+ }
155
169
  throttle(opt) {
156
170
  this.transforms.push(transformThrottle(opt));
157
171
  return this;
@@ -257,18 +271,26 @@ export class Pipeline {
257
271
  this.destination = destination;
258
272
  await this.run();
259
273
  }
260
- async forEach(fn, opt) {
274
+ async forEach(fn, opt = {}) {
261
275
  this.transforms.push(transformMap(fn, {
276
+ predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
262
277
  ...opt,
263
278
  signal: this.abortableSignal,
264
279
  }));
280
+ if (opt.logEvery) {
281
+ this.transforms.push(transformLogProgress(opt));
282
+ }
265
283
  await this.run();
266
284
  }
267
- async forEachSync(fn, opt) {
285
+ async forEachSync(fn, opt = {}) {
268
286
  this.transforms.push(transformMapSync(fn, {
287
+ predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
269
288
  ...opt,
270
289
  signal: this.abortableSignal,
271
290
  }));
291
+ if (opt.logEvery) {
292
+ this.transforms.push(transformLogProgress(opt));
293
+ }
272
294
  await this.run();
273
295
  }
274
296
  async run() {
@@ -1,4 +1,4 @@
1
- import type { ReadableOptions } from 'node:stream';
1
+ import { type ReadableOptions } from 'node:stream';
2
2
  import type { ReadableTyped } from '../stream.model.js';
3
3
  /**
4
4
  * Convenience function to create a Readable that can be pushed into (similar to RxJS Subject).
@@ -12,8 +12,14 @@ import type { ReadableTyped } from '../stream.model.js';
12
12
  * if read() will be called AFTER everything was pushed and Readable is closed (by pushing `null`).
13
13
  * Beware of it when e.g doing unit testing! Jest prefers to hang (not exit-0).
14
14
  */
15
- export declare function readableCreate<T>(items?: Iterable<T>, opt?: ReadableOptions, onRead?: () => void): ReadableTyped<T>;
15
+ export declare function createReadable<T>(items?: Iterable<T>, opt?: ReadableOptions, onRead?: () => void): ReadableTyped<T>;
16
16
  /**
17
17
  * Convenience type-safe wrapper around Readable.from() that infers the Type of input.
18
18
  */
19
- export declare function readableFrom<T>(iterable: Iterable<T> | AsyncIterable<T>, opt?: ReadableOptions): ReadableTyped<T>;
19
+ export declare function createReadableFrom<T>(iterable: Iterable<T> | AsyncIterable<T>, opt?: ReadableOptions): ReadableTyped<T>;
20
+ /**
21
+ * Allows to "create Readable asynchronously".
22
+ * Implemented via a proxy Transform, which is created (and returned) eagerly,
23
+ * and later (when source Readable is created) serves as a pass-through proxy.
24
+ */
25
+ export declare function createReadableFromAsync<T>(fn: () => Promise<ReadableTyped<T>>): ReadableTyped<T>;
@@ -1,3 +1,4 @@
1
+ import { Transform } from 'node:stream';
1
2
  import { Readable } from 'node:stream';
2
3
  /**
3
4
  * Convenience function to create a Readable that can be pushed into (similar to RxJS Subject).
@@ -11,7 +12,7 @@ import { Readable } from 'node:stream';
11
12
  * if read() will be called AFTER everything was pushed and Readable is closed (by pushing `null`).
12
13
  * Beware of it when e.g doing unit testing! Jest prefers to hang (not exit-0).
13
14
  */
14
- export function readableCreate(items = [], opt, onRead) {
15
+ export function createReadable(items = [], opt, onRead) {
15
16
  const readable = new Readable({
16
17
  objectMode: true,
17
18
  ...opt,
@@ -27,6 +28,26 @@ export function readableCreate(items = [], opt, onRead) {
27
28
  /**
28
29
  * Convenience type-safe wrapper around Readable.from() that infers the Type of input.
29
30
  */
30
- export function readableFrom(iterable, opt) {
31
+ export function createReadableFrom(iterable, opt) {
31
32
  return Readable.from(iterable, opt);
32
33
  }
34
+ /**
35
+ * Allows to "create Readable asynchronously".
36
+ * Implemented via a proxy Transform, which is created (and returned) eagerly,
37
+ * and later (when source Readable is created) serves as a pass-through proxy.
38
+ */
39
+ export function createReadableFromAsync(fn) {
40
+ const transform = new Transform({
41
+ objectMode: true,
42
+ highWaterMark: 1,
43
+ transform: (chunk, _encoding, cb) => {
44
+ cb(null, chunk);
45
+ },
46
+ });
47
+ void fn()
48
+ .then(readable => {
49
+ readable.on('error', err => transform.destroy(err)).pipe(transform);
50
+ })
51
+ .catch(err => transform.destroy(err));
52
+ return transform;
53
+ }
@@ -11,11 +11,11 @@ export interface ReadableArrayOptions {
11
11
  /** allows destroying the stream if the signal is aborted. */
12
12
  signal?: AbortSignal;
13
13
  }
14
- export type ReadableMapper<IN, OUT> = (data: IN, opt?: ReadableSignalOptions) => Promisable<OUT>;
15
- export type ReadableFlatMapper<IN, OUT> = (data: IN, opt?: ReadableSignalOptions) => Promisable<OUT[]>;
14
+ export type ReadableMapper<IN, OUT = unknown> = (data: IN, opt?: ReadableSignalOptions) => Promisable<OUT>;
15
+ export type ReadableFlatMapper<IN, OUT = unknown> = (data: IN, opt?: ReadableSignalOptions) => Promisable<OUT[]>;
16
16
  export type ReadableVoidMapper<IN> = (data: IN, opt?: ReadableSignalOptions) => void | Promise<void>;
17
17
  export type ReadablePredicate<IN> = (data: IN, opt?: ReadableSignalOptions) => boolean | Promise<boolean>;
18
- export interface ReadableTyped<T> extends Readable {
18
+ export interface ReadableTyped<T = unknown> extends Readable {
19
19
  toArray: (opt?: ReadableSignalOptions) => Promise<T[]>;
20
20
  map: <OUT>(mapper: ReadableMapper<T, OUT>, opt?: ReadableArrayOptions) => ReadableTyped<OUT>;
21
21
  flatMap: <OUT>(mapper: ReadableFlatMapper<T, OUT>, opt?: ReadableArrayOptions) => ReadableTyped<OUT>;
@@ -26,17 +26,7 @@ export interface ReadableTyped<T> extends Readable {
26
26
  }
27
27
  export interface WritableTyped<T> extends Writable {
28
28
  }
29
- /**
30
- * Type alias that indicates that the Readable is not in objectMode,
31
- * e.g returns a binary stream (like a gzip stream).
32
- */
33
- export type ReadableBinary = Readable;
34
- /**
35
- * Type alias that indicates that the Writable is not in objectMode,
36
- * e.g reads a binary stream (like a gzip stream).
37
- */
38
- export type WritableBinary = Writable;
39
- export interface TransformTyped<IN, OUT> extends Transform {
29
+ export interface TransformTyped<IN = unknown, OUT = unknown> extends Transform {
40
30
  }
41
31
  export interface TransformOptions {
42
32
  /**
@@ -4,7 +4,7 @@ import type { TransformMapOptions } from './transformMap.js';
4
4
  /**
5
5
  * Just a convenience wrapper around `transformMap` that has built-in predicate filtering support.
6
6
  */
7
- export declare function transformFilter<IN = any>(predicate: AsyncPredicate<IN>, opt?: TransformMapOptions): TransformTyped<IN, IN>;
7
+ export declare function transformFilter<IN = any>(asyncPredicate: AsyncPredicate<IN>, opt?: TransformMapOptions): TransformTyped<IN, IN>;
8
8
  /**
9
9
  * Sync version of `transformFilter`
10
10
  */
@@ -3,9 +3,9 @@ import { transformMap } from './transformMap.js';
3
3
  /**
4
4
  * Just a convenience wrapper around `transformMap` that has built-in predicate filtering support.
5
5
  */
6
- export function transformFilter(predicate, opt = {}) {
6
+ export function transformFilter(asyncPredicate, opt = {}) {
7
7
  return transformMap(v => v, {
8
- predicate,
8
+ asyncPredicate,
9
9
  ...opt,
10
10
  });
11
11
  }
@@ -2,7 +2,7 @@ import { Transform } from 'node:stream';
2
2
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
3
3
  import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
4
4
  import { Pipeline } from '../pipeline.js';
5
- import { readableCreate } from '../readable/readableCreate.js';
5
+ import { createReadable } from '../readable/createReadable.js';
6
6
  /**
7
7
  * Allows to "fork" away from the "main pipeline" into the "forked pipeline".
8
8
  *
@@ -14,12 +14,12 @@ export function transformFork(fn, opt = {}) {
14
14
  const { objectMode = true, highWaterMark } = opt;
15
15
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
16
16
  let lock;
17
- const fork = readableCreate([], {}, () => {
17
+ const fork = createReadable([], {}, () => {
18
18
  // `_read` is called
19
19
  if (!lock)
20
20
  return;
21
21
  // We had a lock - let's Resume
22
- logger.log(`TransformFork: resume`);
22
+ logger.debug(`TransformFork: resume`);
23
23
  const lockCopy = lock;
24
24
  lock = undefined;
25
25
  lockCopy.resolve();
@@ -45,7 +45,7 @@ export function transformFork(fn, opt = {}) {
45
45
  if (!shouldContinue && !lock) {
46
46
  // Forked pipeline indicates that we should Pause
47
47
  lock = pDefer();
48
- logger.log(`TransformFork: pause`);
48
+ logger.debug(`TransformFork: pause`);
49
49
  }
50
50
  // acknowledge that we've finished processing the input chunk
51
51
  cb();
@@ -4,7 +4,7 @@ import { progressLogger } from '../progressLogger.js';
4
4
  * Pass-through transform that optionally logs progress.
5
5
  */
6
6
  export function transformLogProgress(opt = {}) {
7
- const { objectMode = true, highWaterMark } = opt;
7
+ const { objectMode = true, highWaterMark = 1 } = opt;
8
8
  const progress = progressLogger(opt);
9
9
  return new Transform({
10
10
  objectMode,
@@ -1,6 +1,6 @@
1
1
  import { type AbortableSignal } from '@naturalcycles/js-lib';
2
2
  import { ErrorMode } from '@naturalcycles/js-lib/error';
3
- import { type AbortableAsyncMapper, type AsyncPredicate, END, type PositiveInteger, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
3
+ import { type AbortableAsyncMapper, type AsyncPredicate, END, type PositiveInteger, type Predicate, type Promisable, SKIP, type StringMap, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
4
4
  import type { TransformOptions, TransformTyped } from '../stream.model.js';
5
5
  export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOptions {
6
6
  /**
@@ -10,7 +10,8 @@ export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOption
10
10
  * Defaults to "pass everything" (including null, undefined, etc).
11
11
  * Simpler way to exclude certain cases is to return SKIP symbol from the mapper.
12
12
  */
13
- predicate?: AsyncPredicate<OUT>;
13
+ predicate?: Predicate<OUT>;
14
+ asyncPredicate?: AsyncPredicate<OUT>;
14
15
  /**
15
16
  * Number of concurrently pending promises returned by `mapper`.
16
17
  *
@@ -23,7 +23,7 @@ import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
23
23
  */
24
24
  export function transformMap(mapper, opt = {}) {
25
25
  const { concurrency = 16, highWaterMark = 64, predicate, // we now default to "no predicate" (meaning pass-everything)
26
- errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', signal, } = opt;
26
+ asyncPredicate, errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', signal, } = opt;
27
27
  const started = Date.now();
28
28
  let index = -1;
29
29
  let countOut = 0;
@@ -94,8 +94,20 @@ export function transformMap(mapper, opt = {}) {
94
94
  // do nothing, don't push
95
95
  return cb();
96
96
  }
97
- if (!predicate || ((await predicate(res, currentIndex)) && !isSettled)) {
98
- // isSettled could have happened in parallel, hence the extra check
97
+ if (predicate) {
98
+ if (predicate(res, currentIndex)) {
99
+ countOut++;
100
+ this.push(res);
101
+ }
102
+ }
103
+ else if (asyncPredicate) {
104
+ if ((await asyncPredicate(res, currentIndex)) && !isSettled) {
105
+ // isSettled could have happened in parallel, hence the extra check
106
+ countOut++;
107
+ this.push(res);
108
+ }
109
+ }
110
+ else {
99
111
  countOut++;
100
112
  this.push(res);
101
113
  }
@@ -1,13 +1,13 @@
1
- import type { CommonLogger } from '@naturalcycles/js-lib/log';
2
- import type { AsyncIndexedMapper } from '@naturalcycles/js-lib/types';
1
+ import type { AsyncIndexedMapper, IndexedMapper } from '@naturalcycles/js-lib/types';
3
2
  import type { TransformOptions, TransformTyped } from '../stream.model.js';
4
- export interface TransformTapOptions extends TransformOptions {
5
- logger?: CommonLogger;
6
- }
7
3
  /**
8
4
  * Similar to RxJS `tap` - allows to run a function for each stream item, without affecting the result.
9
5
  * Item is passed through to the output.
10
6
  *
11
7
  * Can also act as a counter, since `index` is passed to `fn`
12
8
  */
13
- export declare function transformTap<IN>(fn: AsyncIndexedMapper<IN, any>, opt?: TransformTapOptions): TransformTyped<IN, IN>;
9
+ export declare function transformTap<IN>(fn: AsyncIndexedMapper<IN, any>, opt?: TransformOptions): TransformTyped<IN, IN>;
10
+ /**
11
+ * Sync version of transformTap
12
+ */
13
+ export declare function transformTapSync<IN>(fn: IndexedMapper<IN, any>, opt?: TransformOptions): TransformTyped<IN, IN>;
@@ -6,13 +6,12 @@ import { Transform } from 'node:stream';
6
6
  * Can also act as a counter, since `index` is passed to `fn`
7
7
  */
8
8
  export function transformTap(fn, opt = {}) {
9
- const { logger = console } = opt;
9
+ const { logger = console, highWaterMark = 1 } = opt;
10
10
  let index = -1;
11
11
  return new Transform({
12
12
  objectMode: true,
13
- ...opt,
13
+ highWaterMark,
14
14
  async transform(chunk, _, cb) {
15
- // console.log('tap', chunk)
16
15
  try {
17
16
  await fn(chunk, ++index);
18
17
  }
@@ -20,7 +19,28 @@ export function transformTap(fn, opt = {}) {
20
19
  logger.error(err);
21
20
  // suppressed error
22
21
  }
23
- cb(null, chunk); // pass through the item
22
+ cb(null, chunk);
23
+ },
24
+ });
25
+ }
26
+ /**
27
+ * Sync version of transformTap
28
+ */
29
+ export function transformTapSync(fn, opt = {}) {
30
+ const { logger = console, highWaterMark = 1 } = opt;
31
+ let index = -1;
32
+ return new Transform({
33
+ objectMode: true,
34
+ highWaterMark,
35
+ transform(chunk, _, cb) {
36
+ try {
37
+ fn(chunk, ++index);
38
+ }
39
+ catch (err) {
40
+ logger.error(err);
41
+ // suppressed error
42
+ }
43
+ cb(null, chunk);
24
44
  },
25
45
  });
26
46
  }
@@ -5,9 +5,10 @@ import { Writable } from 'node:stream';
5
5
  * Put it in the end of your pipeline in case it ends with Transform that needs a consumer.
6
6
  */
7
7
  export function writableVoid(opt = {}) {
8
+ const { objectMode = true, highWaterMark = 1 } = opt;
8
9
  return new Writable({
9
- objectMode: true,
10
- ...opt,
10
+ objectMode,
11
+ highWaterMark,
11
12
  write(_chunk, _, cb) {
12
13
  cb();
13
14
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.27.3",
4
+ "version": "15.29.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -5,8 +5,8 @@ export * from './ndjson/transformJsonParse.js'
5
5
  export * from './ndjson/transformToNDJson.js'
6
6
  export * from './pipeline.js'
7
7
  export * from './progressLogger.js'
8
+ export * from './readable/createReadable.js'
8
9
  export * from './readable/readableCombined.js'
9
- export * from './readable/readableCreate.js'
10
10
  export * from './readable/readableFromArray.js'
11
11
  export * from './stream.model.js'
12
12
  export * from './transform/transformChunk.js'
@@ -4,22 +4,24 @@ import type { ReadableStream as WebReadableStream } from 'node:stream/web'
4
4
  import { createUnzip, type ZlibOptions } from 'node:zlib'
5
5
  import { createGzip } from 'node:zlib'
6
6
  import { createAbortableSignal } from '@naturalcycles/js-lib'
7
- import type {
8
- AbortableAsyncMapper,
9
- AsyncIndexedMapper,
10
- AsyncPredicate,
11
- END,
12
- IndexedMapper,
13
- Integer,
14
- NonNegativeInteger,
15
- PositiveInteger,
16
- Predicate,
17
- SKIP,
7
+ import {
8
+ _passthroughPredicate,
9
+ type AbortableAsyncMapper,
10
+ type AsyncIndexedMapper,
11
+ type AsyncPredicate,
12
+ type END,
13
+ type IndexedMapper,
14
+ type Integer,
15
+ type NonNegativeInteger,
16
+ type PositiveInteger,
17
+ type Predicate,
18
+ type SKIP,
18
19
  } from '@naturalcycles/js-lib/types'
19
20
  import { fs2 } from '../fs/fs2.js'
20
21
  import { createReadStreamAsNDJson } from './ndjson/createReadStreamAsNDJson.js'
21
22
  import { transformJsonParse } from './ndjson/transformJsonParse.js'
22
23
  import { transformToNDJson } from './ndjson/transformToNDJson.js'
24
+ import { createReadableFromAsync } from './readable/createReadable.js'
23
25
  import type {
24
26
  ReadableTyped,
25
27
  TransformOptions,
@@ -44,12 +46,12 @@ import {
44
46
  import { transformMapSync, type TransformMapSyncOptions } from './transform/transformMapSync.js'
45
47
  import { transformOffset, type TransformOffsetOptions } from './transform/transformOffset.js'
46
48
  import { transformSplitOnNewline } from './transform/transformSplit.js'
47
- import { transformTap, type TransformTapOptions } from './transform/transformTap.js'
49
+ import { transformTap, transformTapSync } from './transform/transformTap.js'
48
50
  import { transformThrottle, type TransformThrottleOptions } from './transform/transformThrottle.js'
49
51
  import { writablePushToArray } from './writable/writablePushToArray.js'
50
52
  import { writableVoid } from './writable/writableVoid.js'
51
53
 
52
- export class Pipeline<T> {
54
+ export class Pipeline<T = unknown> {
53
55
  // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
54
56
  private readonly source: Readable
55
57
  private transforms: NodeJS.ReadWriteStream[] = []
@@ -68,6 +70,15 @@ export class Pipeline<T> {
68
70
  return new Pipeline(source)
69
71
  }
70
72
 
73
+ /**
74
+ * Useful in cases when Readable is not immediately available,
75
+ * but only available after an async operation is completed.
76
+ * Implemented via a proxy Transform, which should be transparent.
77
+ */
78
+ static fromAsyncReadable<T = unknown>(fn: () => Promise<ReadableTyped<T>>): Pipeline<T> {
79
+ return new Pipeline(createReadableFromAsync(fn))
80
+ }
81
+
71
82
  static fromWeb<T>(webReadableStream: WebReadableStream<T>): Pipeline<T> {
72
83
  return new Pipeline(Readable.fromWeb(webReadableStream))
73
84
  }
@@ -190,10 +201,10 @@ export class Pipeline<T> {
190
201
  return this as any
191
202
  }
192
203
 
193
- filter(predicate: AsyncPredicate<T>, opt?: TransformMapOptions): this {
204
+ filter(asyncPredicate: AsyncPredicate<T>, opt?: TransformMapOptions): this {
194
205
  this.transforms.push(
195
206
  transformMap(v => v, {
196
- predicate,
207
+ asyncPredicate,
197
208
  ...opt,
198
209
  signal: this.abortableSignal,
199
210
  }),
@@ -211,11 +222,16 @@ export class Pipeline<T> {
211
222
  return this
212
223
  }
213
224
 
214
- tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformTapOptions): this {
225
+ tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformOptions): this {
215
226
  this.transforms.push(transformTap(fn, opt))
216
227
  return this
217
228
  }
218
229
 
230
+ tapSync(fn: IndexedMapper<T, any>, opt?: TransformOptions): this {
231
+ this.transforms.push(transformTapSync(fn, opt))
232
+ return this
233
+ }
234
+
219
235
  throttle(opt: TransformThrottleOptions): this {
220
236
  this.transforms.push(transformThrottle(opt))
221
237
  return this
@@ -347,27 +363,35 @@ export class Pipeline<T> {
347
363
 
348
364
  async forEach(
349
365
  fn: AsyncIndexedMapper<T, void>,
350
- opt?: TransformMapOptions<T, void>,
366
+ opt: TransformMapOptions<T, void> & TransformLogProgressOptions<T> = {},
351
367
  ): Promise<void> {
352
368
  this.transforms.push(
353
369
  transformMap(fn, {
370
+ predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
354
371
  ...opt,
355
372
  signal: this.abortableSignal,
356
373
  }),
357
374
  )
375
+ if (opt.logEvery) {
376
+ this.transforms.push(transformLogProgress(opt))
377
+ }
358
378
  await this.run()
359
379
  }
360
380
 
361
381
  async forEachSync(
362
382
  fn: IndexedMapper<T, void>,
363
- opt?: TransformMapSyncOptions<T, void>,
383
+ opt: TransformMapSyncOptions<T, void> & TransformLogProgressOptions<T> = {},
364
384
  ): Promise<void> {
365
385
  this.transforms.push(
366
386
  transformMapSync(fn, {
387
+ predicate: opt.logEvery ? _passthroughPredicate : undefined, // for the logger to work
367
388
  ...opt,
368
389
  signal: this.abortableSignal,
369
390
  }),
370
391
  )
392
+ if (opt.logEvery) {
393
+ this.transforms.push(transformLogProgress(opt))
394
+ }
371
395
  await this.run()
372
396
  }
373
397
 
@@ -1,4 +1,4 @@
1
- import type { ReadableOptions } from 'node:stream'
1
+ import { type ReadableOptions, Transform } from 'node:stream'
2
2
  import { Readable } from 'node:stream'
3
3
  import type { ReadableTyped } from '../stream.model.js'
4
4
 
@@ -14,7 +14,7 @@ import type { ReadableTyped } from '../stream.model.js'
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
16
  */
17
- export function readableCreate<T>(
17
+ export function createReadable<T>(
18
18
  items: Iterable<T> = [],
19
19
  opt?: ReadableOptions,
20
20
  onRead?: () => void, // read callback
@@ -35,9 +35,32 @@ export function readableCreate<T>(
35
35
  /**
36
36
  * Convenience type-safe wrapper around Readable.from() that infers the Type of input.
37
37
  */
38
- export function readableFrom<T>(
38
+ export function createReadableFrom<T>(
39
39
  iterable: Iterable<T> | AsyncIterable<T>,
40
40
  opt?: ReadableOptions,
41
41
  ): ReadableTyped<T> {
42
42
  return Readable.from(iterable, opt)
43
43
  }
44
+
45
+ /**
46
+ * Allows to "create Readable asynchronously".
47
+ * Implemented via a proxy Transform, which is created (and returned) eagerly,
48
+ * and later (when source Readable is created) serves as a pass-through proxy.
49
+ */
50
+ export function createReadableFromAsync<T>(fn: () => Promise<ReadableTyped<T>>): ReadableTyped<T> {
51
+ const transform = new Transform({
52
+ objectMode: true,
53
+ highWaterMark: 1,
54
+ transform: (chunk, _encoding, cb) => {
55
+ cb(null, chunk)
56
+ },
57
+ })
58
+
59
+ void fn()
60
+ .then(readable => {
61
+ readable.on('error', err => transform.destroy(err)).pipe(transform)
62
+ })
63
+ .catch(err => transform.destroy(err))
64
+
65
+ return transform
66
+ }
@@ -14,9 +14,12 @@ export interface ReadableArrayOptions {
14
14
  signal?: AbortSignal
15
15
  }
16
16
 
17
- export type ReadableMapper<IN, OUT> = (data: IN, opt?: ReadableSignalOptions) => Promisable<OUT>
17
+ export type ReadableMapper<IN, OUT = unknown> = (
18
+ data: IN,
19
+ opt?: ReadableSignalOptions,
20
+ ) => Promisable<OUT>
18
21
 
19
- export type ReadableFlatMapper<IN, OUT> = (
22
+ export type ReadableFlatMapper<IN, OUT = unknown> = (
20
23
  data: IN,
21
24
  opt?: ReadableSignalOptions,
22
25
  ) => Promisable<OUT[]>
@@ -28,7 +31,7 @@ export type ReadablePredicate<IN> = (
28
31
  opt?: ReadableSignalOptions,
29
32
  ) => boolean | Promise<boolean>
30
33
 
31
- export interface ReadableTyped<T> extends Readable {
34
+ export interface ReadableTyped<T = unknown> extends Readable {
32
35
  toArray: (opt?: ReadableSignalOptions) => Promise<T[]>
33
36
 
34
37
  map: <OUT>(mapper: ReadableMapper<T, OUT>, opt?: ReadableArrayOptions) => ReadableTyped<OUT>
@@ -49,19 +52,8 @@ export interface ReadableTyped<T> extends Readable {
49
52
  // biome-ignore lint/correctness/noUnusedVariables: ok
50
53
  export interface WritableTyped<T> extends Writable {}
51
54
 
52
- /**
53
- * Type alias that indicates that the Readable is not in objectMode,
54
- * e.g returns a binary stream (like a gzip stream).
55
- */
56
- export type ReadableBinary = Readable
57
- /**
58
- * Type alias that indicates that the Writable is not in objectMode,
59
- * e.g reads a binary stream (like a gzip stream).
60
- */
61
- export type WritableBinary = Writable
62
-
63
55
  // biome-ignore lint/correctness/noUnusedVariables: ok
64
- export interface TransformTyped<IN, OUT> extends Transform {}
56
+ export interface TransformTyped<IN = unknown, OUT = unknown> extends Transform {}
65
57
 
66
58
  export interface TransformOptions {
67
59
  /**
@@ -8,11 +8,11 @@ import { transformMap } from './transformMap.js'
8
8
  * Just a convenience wrapper around `transformMap` that has built-in predicate filtering support.
9
9
  */
10
10
  export function transformFilter<IN = any>(
11
- predicate: AsyncPredicate<IN>,
11
+ asyncPredicate: AsyncPredicate<IN>,
12
12
  opt: TransformMapOptions = {},
13
13
  ): TransformTyped<IN, IN> {
14
14
  return transformMap(v => v, {
15
- predicate,
15
+ asyncPredicate,
16
16
  ...opt,
17
17
  })
18
18
  }
@@ -2,7 +2,7 @@ import { Transform } from 'node:stream'
2
2
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
3
3
  import { type DeferredPromise, pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
4
4
  import { Pipeline } from '../pipeline.js'
5
- import { readableCreate } from '../readable/readableCreate.js'
5
+ import { createReadable } from '../readable/createReadable.js'
6
6
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
7
7
 
8
8
  /**
@@ -21,11 +21,11 @@ export function transformFork<T>(
21
21
 
22
22
  let lock: DeferredPromise | undefined
23
23
 
24
- const fork = readableCreate<T>([], {}, () => {
24
+ const fork = createReadable<T>([], {}, () => {
25
25
  // `_read` is called
26
26
  if (!lock) return
27
27
  // We had a lock - let's Resume
28
- logger.log(`TransformFork: resume`)
28
+ logger.debug(`TransformFork: resume`)
29
29
  const lockCopy = lock
30
30
  lock = undefined
31
31
  lockCopy.resolve()
@@ -54,7 +54,7 @@ export function transformFork<T>(
54
54
  if (!shouldContinue && !lock) {
55
55
  // Forked pipeline indicates that we should Pause
56
56
  lock = pDefer()
57
- logger.log(`TransformFork: pause`)
57
+ logger.debug(`TransformFork: pause`)
58
58
  }
59
59
 
60
60
  // acknowledge that we've finished processing the input chunk
@@ -13,7 +13,7 @@ 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
+ const { objectMode = true, highWaterMark = 1 } = opt
17
17
  const progress = progressLogger(opt)
18
18
 
19
19
  return new Transform({
@@ -8,6 +8,7 @@ import {
8
8
  type AsyncPredicate,
9
9
  END,
10
10
  type PositiveInteger,
11
+ type Predicate,
11
12
  type Promisable,
12
13
  SKIP,
13
14
  type StringMap,
@@ -26,7 +27,9 @@ export interface TransformMapOptions<IN = any, OUT = IN> extends TransformOption
26
27
  * Defaults to "pass everything" (including null, undefined, etc).
27
28
  * Simpler way to exclude certain cases is to return SKIP symbol from the mapper.
28
29
  */
29
- predicate?: AsyncPredicate<OUT>
30
+ predicate?: Predicate<OUT>
31
+
32
+ asyncPredicate?: AsyncPredicate<OUT>
30
33
 
31
34
  /**
32
35
  * Number of concurrently pending promises returned by `mapper`.
@@ -137,6 +140,7 @@ export function transformMap<IN = any, OUT = IN>(
137
140
  concurrency = 16,
138
141
  highWaterMark = 64,
139
142
  predicate, // we now default to "no predicate" (meaning pass-everything)
143
+ asyncPredicate,
140
144
  errorMode = ErrorMode.THROW_IMMEDIATELY,
141
145
  onError,
142
146
  onDone,
@@ -226,8 +230,18 @@ export function transformMap<IN = any, OUT = IN>(
226
230
  return cb()
227
231
  }
228
232
 
229
- if (!predicate || ((await predicate(res, currentIndex)) && !isSettled)) {
230
- // isSettled could have happened in parallel, hence the extra check
233
+ if (predicate) {
234
+ if (predicate(res, currentIndex)) {
235
+ countOut++
236
+ this.push(res)
237
+ }
238
+ } else if (asyncPredicate) {
239
+ if ((await asyncPredicate(res, currentIndex)) && !isSettled) {
240
+ // isSettled could have happened in parallel, hence the extra check
241
+ countOut++
242
+ this.push(res)
243
+ }
244
+ } else {
231
245
  countOut++
232
246
  this.push(res)
233
247
  }
@@ -1,12 +1,7 @@
1
1
  import { Transform } from 'node:stream'
2
- import type { CommonLogger } from '@naturalcycles/js-lib/log'
3
- import type { AsyncIndexedMapper } from '@naturalcycles/js-lib/types'
2
+ import type { AsyncIndexedMapper, IndexedMapper } from '@naturalcycles/js-lib/types'
4
3
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
5
4
 
6
- export interface TransformTapOptions extends TransformOptions {
7
- logger?: CommonLogger
8
- }
9
-
10
5
  /**
11
6
  * Similar to RxJS `tap` - allows to run a function for each stream item, without affecting the result.
12
7
  * Item is passed through to the output.
@@ -15,17 +10,15 @@ export interface TransformTapOptions extends TransformOptions {
15
10
  */
16
11
  export function transformTap<IN>(
17
12
  fn: AsyncIndexedMapper<IN, any>,
18
- opt: TransformTapOptions = {},
13
+ opt: TransformOptions = {},
19
14
  ): TransformTyped<IN, IN> {
20
- const { logger = console } = opt
15
+ const { logger = console, highWaterMark = 1 } = opt
21
16
  let index = -1
22
17
 
23
18
  return new Transform({
24
19
  objectMode: true,
25
- ...opt,
20
+ highWaterMark,
26
21
  async transform(chunk: IN, _, cb) {
27
- // console.log('tap', chunk)
28
-
29
22
  try {
30
23
  await fn(chunk, ++index)
31
24
  } catch (err) {
@@ -33,7 +26,33 @@ export function transformTap<IN>(
33
26
  // suppressed error
34
27
  }
35
28
 
36
- cb(null, chunk) // pass through the item
29
+ cb(null, chunk)
30
+ },
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Sync version of transformTap
36
+ */
37
+ export function transformTapSync<IN>(
38
+ fn: IndexedMapper<IN, any>,
39
+ opt: TransformOptions = {},
40
+ ): TransformTyped<IN, IN> {
41
+ const { logger = console, highWaterMark = 1 } = opt
42
+ let index = -1
43
+
44
+ return new Transform({
45
+ objectMode: true,
46
+ highWaterMark,
47
+ transform(chunk: IN, _, cb) {
48
+ try {
49
+ fn(chunk, ++index)
50
+ } catch (err) {
51
+ logger.error(err)
52
+ // suppressed error
53
+ }
54
+
55
+ cb(null, chunk)
37
56
  },
38
57
  })
39
58
  }
@@ -7,9 +7,10 @@ import type { TransformOptions } from '../stream.model.js'
7
7
  * Put it in the end of your pipeline in case it ends with Transform that needs a consumer.
8
8
  */
9
9
  export function writableVoid(opt: TransformOptions = {}): Writable {
10
+ const { objectMode = true, highWaterMark = 1 } = opt
10
11
  return new Writable({
11
- objectMode: true,
12
- ...opt,
12
+ objectMode,
13
+ highWaterMark,
13
14
  write(_chunk, _, cb) {
14
15
  cb()
15
16
  },