@naturalcycles/nodejs-lib 15.22.0 → 15.23.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 (45) hide show
  1. package/dist/exec2/exec2.js +1 -0
  2. package/dist/stream/index.d.ts +1 -2
  3. package/dist/stream/index.js +1 -2
  4. package/dist/stream/ndjson/ndjsonMap.d.ts +1 -1
  5. package/dist/stream/ndjson/ndjsonMap.js +13 -15
  6. package/dist/stream/ndjson/ndjsonStreamForEach.d.ts +2 -2
  7. package/dist/stream/ndjson/ndjsonStreamForEach.js +9 -15
  8. package/dist/stream/pipeline.d.ts +79 -0
  9. package/dist/stream/pipeline.js +220 -0
  10. package/dist/stream/stream.util.d.ts +1 -3
  11. package/dist/stream/stream.util.js +1 -20
  12. package/dist/stream/transform/transformChunk.d.ts +5 -8
  13. package/dist/stream/transform/transformChunk.js +4 -2
  14. package/dist/stream/transform/transformFlatten.d.ts +1 -0
  15. package/dist/stream/transform/transformFlatten.js +15 -4
  16. package/dist/stream/transform/transformLimit.d.ts +3 -26
  17. package/dist/stream/transform/transformLimit.js +14 -23
  18. package/dist/stream/transform/transformMap.d.ts +5 -0
  19. package/dist/stream/transform/transformMap.js +22 -18
  20. package/dist/stream/transform/transformMapSync.d.ts +5 -3
  21. package/dist/stream/transform/transformMapSync.js +7 -8
  22. package/dist/stream/transform/transformTee.js +4 -2
  23. package/dist/stream/writable/writableForEach.d.ts +2 -1
  24. package/dist/stream/writable/writableFork.js +2 -2
  25. package/package.json +1 -1
  26. package/src/exec2/exec2.ts +1 -0
  27. package/src/stream/index.ts +1 -2
  28. package/src/stream/ndjson/ndjsonMap.ts +12 -22
  29. package/src/stream/ndjson/ndjsonStreamForEach.ts +8 -15
  30. package/src/stream/pipeline.ts +301 -0
  31. package/src/stream/stream.util.ts +1 -29
  32. package/src/stream/transform/transformChunk.ts +8 -11
  33. package/src/stream/transform/transformFlatten.ts +16 -4
  34. package/src/stream/transform/transformLimit.ts +20 -51
  35. package/src/stream/transform/transformMap.ts +31 -20
  36. package/src/stream/transform/transformMapSync.ts +14 -8
  37. package/src/stream/transform/transformTee.ts +5 -2
  38. package/src/stream/writable/writableForEach.ts +2 -2
  39. package/src/stream/writable/writableFork.ts +2 -2
  40. package/dist/stream/pipeline/pipeline.d.ts +0 -36
  41. package/dist/stream/pipeline/pipeline.js +0 -82
  42. package/dist/stream/readable/readableForEach.d.ts +0 -19
  43. package/dist/stream/readable/readableForEach.js +0 -30
  44. package/src/stream/pipeline/pipeline.ts +0 -114
  45. package/src/stream/readable/readableForEach.ts +0 -42
@@ -1,11 +1,11 @@
1
1
  import { _hc } from '@naturalcycles/js-lib';
2
2
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
3
- import { _anyToError, ErrorMode } from '@naturalcycles/js-lib/error';
3
+ import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
4
4
  import { _stringify } from '@naturalcycles/js-lib/string/stringify.js';
5
5
  import { END, SKIP, } from '@naturalcycles/js-lib/types';
6
6
  import through2Concurrent from 'through2-concurrent';
7
7
  import { yellow } from '../../colors/colors.js';
8
- import { pipelineClose } from '../stream.util.js';
8
+ import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
9
9
  // doesn't work, cause here we don't construct our Transform instance ourselves
10
10
  // export class TransformMap extends AbortableTransform {}
11
11
  /**
@@ -22,11 +22,12 @@ import { pipelineClose } from '../stream.util.js';
22
22
  */
23
23
  export function transformMap(mapper, opt = {}) {
24
24
  const { concurrency = 16, highWaterMark = 64, predicate, // we now default to "no predicate" (meaning pass-everything)
25
- errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', logger = console, } = opt;
25
+ errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', logger = console, signal, } = opt;
26
26
  const started = Date.now();
27
27
  let index = -1;
28
28
  let countOut = 0;
29
29
  let isSettled = false;
30
+ let ok = true;
30
31
  let errors = 0;
31
32
  const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
32
33
  return through2Concurrent.obj({
@@ -57,7 +58,7 @@ export function transformMap(mapper, opt = {}) {
57
58
  // emit no error
58
59
  try {
59
60
  await onDone?.({
60
- ok: true,
61
+ ok,
61
62
  collectedErrors,
62
63
  countErrors: errors,
63
64
  countIn: index + 1,
@@ -84,7 +85,8 @@ export function transformMap(mapper, opt = {}) {
84
85
  if (res === END) {
85
86
  isSettled = true;
86
87
  logger.log(`transformMap END received at index ${currentIndex}`);
87
- pipelineClose('transformMap', this, this.sourceReadable, this.streamDone, logger);
88
+ _assert(signal, 'signal is required when using END');
89
+ signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
88
90
  return cb();
89
91
  }
90
92
  if (res === SKIP) {
@@ -110,19 +112,21 @@ export function transformMap(mapper, opt = {}) {
110
112
  }
111
113
  if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
112
114
  isSettled = true;
113
- try {
114
- await onDone?.({
115
- ok: false,
116
- collectedErrors,
117
- countErrors: errors,
118
- countIn: index + 1,
119
- countOut,
120
- started,
121
- });
122
- }
123
- catch (err) {
124
- logger.error(err);
125
- }
115
+ ok = false;
116
+ // Tests show that onDone is still called at `final` (second time),
117
+ // so, we no longer call it here
118
+ // try {
119
+ // await onDone?.({
120
+ // ok: false,
121
+ // collectedErrors,
122
+ // countErrors: errors,
123
+ // countIn: index + 1,
124
+ // countOut,
125
+ // started,
126
+ // })
127
+ // } catch (err) {
128
+ // logger.error(err)
129
+ // }
126
130
  return cb(err); // Emit error immediately
127
131
  }
128
132
  if (errorMode === ErrorMode.THROW_AGGREGATED) {
@@ -1,8 +1,8 @@
1
+ import type { AbortableSignal } from '@naturalcycles/js-lib';
1
2
  import { ErrorMode } from '@naturalcycles/js-lib/error';
2
3
  import type { CommonLogger } from '@naturalcycles/js-lib/log';
3
4
  import type { IndexedMapper, Predicate } from '@naturalcycles/js-lib/types';
4
5
  import { END, SKIP } from '@naturalcycles/js-lib/types';
5
- import { AbortableTransform } from '../pipeline/pipeline.js';
6
6
  import type { TransformTyped } from '../stream.model.js';
7
7
  import type { TransformMapStats } from './transformMap.js';
8
8
  export interface TransformMapSyncOptions<IN = any, OUT = IN> {
@@ -45,8 +45,10 @@ export interface TransformMapSyncOptions<IN = any, OUT = IN> {
45
45
  */
46
46
  metric?: string;
47
47
  logger?: CommonLogger;
48
- }
49
- export declare class TransformMapSync extends AbortableTransform {
48
+ /**
49
+ * Allows to abort (gracefully stop) the stream from inside the Transform.
50
+ */
51
+ signal?: AbortableSignal;
50
52
  }
51
53
  /**
52
54
  * Sync (not async) version of transformMap.
@@ -1,24 +1,22 @@
1
- import { _anyToError, ErrorMode } from '@naturalcycles/js-lib/error';
1
+ import { Transform } from 'node:stream';
2
+ import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
2
3
  import { END, SKIP } from '@naturalcycles/js-lib/types';
3
4
  import { yellow } from '../../colors/colors.js';
4
- import { AbortableTransform } from '../pipeline/pipeline.js';
5
- import { pipelineClose } from '../stream.util.js';
6
- export class TransformMapSync extends AbortableTransform {
7
- }
5
+ import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
8
6
  /**
9
7
  * Sync (not async) version of transformMap.
10
8
  * Supposedly faster, for cases when async is not needed.
11
9
  */
12
10
  export function transformMapSync(mapper, opt = {}) {
13
11
  const { predicate, // defaults to "no predicate" (pass everything)
14
- errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', objectMode = true, logger = console, } = opt;
12
+ errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', objectMode = true, logger = console, signal, } = opt;
15
13
  const started = Date.now();
16
14
  let index = -1;
17
15
  let countOut = 0;
18
16
  let isSettled = false;
19
17
  let errors = 0;
20
18
  const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
21
- return new TransformMapSync({
19
+ return new Transform({
22
20
  objectMode,
23
21
  ...opt,
24
22
  transform(chunk, _, cb) {
@@ -32,7 +30,8 @@ export function transformMapSync(mapper, opt = {}) {
32
30
  if (v === END) {
33
31
  isSettled = true; // will be checked later
34
32
  logger.log(`transformMapSync END received at index ${currentIndex}`);
35
- pipelineClose('transformMapSync', this, this.sourceReadable, this.streamDone, logger);
33
+ _assert(signal, 'signal is required when using END');
34
+ signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
36
35
  return cb();
37
36
  }
38
37
  if (v === SKIP) {
@@ -1,5 +1,5 @@
1
1
  import { Transform } from 'node:stream';
2
- import { _pipeline } from '../pipeline/pipeline.js';
2
+ import { pipeline } from 'node:stream/promises';
3
3
  import { readableCreate } from '../readable/readableCreate.js';
4
4
  /**
5
5
  * Allows to "tee"/"fork" away from the "main pipeline" into the "secondary pipeline".
@@ -12,10 +12,12 @@ import { readableCreate } from '../readable/readableCreate.js';
12
12
  */
13
13
  export function transformTee(streams) {
14
14
  const readable = readableCreate();
15
- const secondPipelinePromise = _pipeline([readable, ...streams]);
15
+ const secondPipelinePromise = pipeline([readable, ...streams]);
16
16
  return new Transform({
17
17
  objectMode: true,
18
18
  transform(chunk, _, cb) {
19
+ // todo: it's possible to start respecting backpressure,
20
+ // if we start to listen to the boolean output of .push()
19
21
  // pass to the "secondary" pipeline
20
22
  readable.push(chunk);
21
23
  // pass through to the "main" pipeline
@@ -1,6 +1,7 @@
1
1
  import type { AsyncIndexedMapper, IndexedMapper } from '@naturalcycles/js-lib/types';
2
2
  import type { WritableTyped } from '../stream.model.js';
3
3
  import { type TransformMapOptions } from '../transform/transformMap.js';
4
+ import { type TransformMapSyncOptions } from '../transform/transformMapSync.js';
4
5
  /**
5
6
  * Just an alias to transformMap that declares OUT as void.
6
7
  */
@@ -8,4 +9,4 @@ export declare function writableForEach<IN = any>(mapper: AsyncIndexedMapper<IN,
8
9
  /**
9
10
  * Just an alias to transformMap that declares OUT as void.
10
11
  */
11
- export declare function writableForEachSync<IN = any>(mapper: IndexedMapper<IN, void>, opt?: TransformMapOptions<IN, void>): WritableTyped<IN>;
12
+ export declare function writableForEachSync<IN = any>(mapper: IndexedMapper<IN, void>, opt?: TransformMapSyncOptions<IN, void>): WritableTyped<IN>;
@@ -1,5 +1,5 @@
1
1
  import { Writable } from 'node:stream';
2
- import { _pipeline } from '../pipeline/pipeline.js';
2
+ import { pipeline } from 'node:stream/promises';
3
3
  import { readableCreate } from '../readable/readableCreate.js';
4
4
  /**
5
5
  * Allows "forking" a stream inside pipeline into a number of pipeline chains (2 or more).
@@ -14,7 +14,7 @@ export function writableFork(chains, opt) {
14
14
  const allChainsDone = Promise.all(chains.map(async (chain) => {
15
15
  const readable = readableCreate();
16
16
  readables.push(readable);
17
- return await _pipeline([readable, ...chain]);
17
+ return await pipeline([readable, ...chain]);
18
18
  })).catch(err => {
19
19
  console.error(err); // ensure the error is logged
20
20
  throw err;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.22.0",
4
+ "version": "15.23.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -288,6 +288,7 @@ class Exec2 {
288
288
  [
289
289
  ' ',
290
290
  dimGrey(envString),
291
+ // todo: only before first space
291
292
  white(_substringAfterLast(cmd, '/')),
292
293
  ...((opt as SpawnOptions).args || []),
293
294
  ]
@@ -5,11 +5,10 @@ export * from './ndjson/ndjsonMap.js'
5
5
  export * from './ndjson/ndjsonStreamForEach.js'
6
6
  export * from './ndjson/transformJsonParse.js'
7
7
  export * from './ndjson/transformToNDJson.js'
8
- export * from './pipeline/pipeline.js'
8
+ export * from './pipeline.js'
9
9
  export * from './progressLogger.js'
10
10
  export * from './readable/readableCombined.js'
11
11
  export * from './readable/readableCreate.js'
12
- export * from './readable/readableForEach.js'
13
12
  export * from './readable/readableFromArray.js'
14
13
  export * from './readable/readableToArray.js'
15
14
  export * from './stream.model.js'
@@ -1,13 +1,7 @@
1
1
  import { ErrorMode } from '@naturalcycles/js-lib/error/errorMode.js'
2
2
  import type { AbortableAsyncMapper } from '@naturalcycles/js-lib/types'
3
- import {
4
- createReadStreamAsNDJSON,
5
- createWriteStreamAsNDJSON,
6
- transformFlatten,
7
- type TransformLogProgressOptions,
8
- type TransformMapOptions,
9
- } from '../index.js'
10
- import { _pipeline, transformLimit, transformLogProgress, transformMap } from '../index.js'
3
+ import type { TransformLogProgressOptions, TransformMapOptions } from '../index.js'
4
+ import { Pipeline } from '../pipeline.js'
11
5
 
12
6
  export interface NDJSONMapOptions<IN = any, OUT = IN>
13
7
  extends TransformMapOptions<IN, OUT>,
@@ -39,20 +33,16 @@ export async function ndjsonMap<IN = any, OUT = any>(
39
33
  outputFilePath,
40
34
  })
41
35
 
42
- const readable = createReadStreamAsNDJSON(inputFilePath).take(
43
- limitInput || Number.POSITIVE_INFINITY,
44
- )
45
-
46
- await _pipeline([
47
- readable,
48
- transformLogProgress({ metric: 'read', ...opt }),
49
- transformMap(mapper, {
36
+ await Pipeline.fromNDJsonFile<IN>(inputFilePath)
37
+ .limitSource(limitInput)
38
+ .logProgress({ metric: 'read', ...opt })
39
+ .map(mapper, {
50
40
  errorMode: ErrorMode.SUPPRESS,
51
41
  ...opt,
52
- }),
53
- transformFlatten(),
54
- transformLimit({ limit: limitOutput, sourceReadable: readable }),
55
- transformLogProgress({ metric: 'saved', logEvery: logEveryOutput }),
56
- ...createWriteStreamAsNDJSON(outputFilePath),
57
- ])
42
+ })
43
+ .flattenIfNeeded()
44
+ // .typeCastAs<OUT>()
45
+ .limit(limitOutput)
46
+ .logProgress({ metric: 'saved', logEvery: logEveryOutput })
47
+ .toNDJsonFile(outputFilePath)
58
48
  }
@@ -1,13 +1,8 @@
1
1
  import { ErrorMode } from '@naturalcycles/js-lib/error/errorMode.js'
2
2
  import type { AbortableAsyncMapper } from '@naturalcycles/js-lib/types'
3
- import { _pipeline } from '../pipeline/pipeline.js'
4
- import {
5
- transformLogProgress,
6
- type TransformLogProgressOptions,
7
- } from '../transform/transformLogProgress.js'
8
- import { transformMap, type TransformMapOptions } from '../transform/transformMap.js'
9
- import { writableVoid } from '../writable/writableVoid.js'
10
- import { createReadStreamAsNDJSON } from './createReadStreamAsNDJSON.js'
3
+ import { Pipeline } from '../pipeline.js'
4
+ import type { TransformLogProgressOptions } from '../transform/transformLogProgress.js'
5
+ import type { TransformMapOptions } from '../transform/transformMap.js'
11
6
 
12
7
  export interface NDJSONStreamForEachOptions<IN = any>
13
8
  extends TransformMapOptions<IN, void>,
@@ -22,14 +17,12 @@ export async function ndjsonStreamForEach<T>(
22
17
  mapper: AbortableAsyncMapper<T, void>,
23
18
  opt: NDJSONStreamForEachOptions<T>,
24
19
  ): Promise<void> {
25
- await _pipeline([
26
- createReadStreamAsNDJSON(opt.inputFilePath),
27
- transformMap<T, any>(mapper, {
20
+ await Pipeline.fromNDJsonFile<T>(opt.inputFilePath)
21
+ .map(mapper, {
28
22
  errorMode: ErrorMode.THROW_AGGREGATED,
29
23
  ...opt,
30
24
  predicate: () => true, // to log progress properly
31
- }),
32
- transformLogProgress(opt),
33
- writableVoid(),
34
- ])
25
+ })
26
+ .logProgress(opt)
27
+ .run()
35
28
  }
@@ -0,0 +1,301 @@
1
+ import { Readable, type Transform } from 'node:stream'
2
+ import { pipeline } from 'node:stream/promises'
3
+ import { createGzip } from 'node:zlib'
4
+ import { createAbortableSignal } from '@naturalcycles/js-lib'
5
+ import type {
6
+ AbortableAsyncMapper,
7
+ AsyncIndexedMapper,
8
+ AsyncPredicate,
9
+ END,
10
+ IndexedMapper,
11
+ Integer,
12
+ NonNegativeInteger,
13
+ PositiveInteger,
14
+ Predicate,
15
+ SKIP,
16
+ } from '@naturalcycles/js-lib/types'
17
+ import { fs2 } from '../fs/fs2.js'
18
+ import { createReadStreamAsNDJSON } from './ndjson/createReadStreamAsNDJSON.js'
19
+ import { transformToNDJson } from './ndjson/transformToNDJson.js'
20
+ import type {
21
+ ReadableTyped,
22
+ TransformOptions,
23
+ TransformTyped,
24
+ WritableTyped,
25
+ } from './stream.model.js'
26
+ import { PIPELINE_GRACEFUL_ABORT } from './stream.util.js'
27
+ import { transformChunk } from './transform/transformChunk.js'
28
+ import { transformFilterSync } from './transform/transformFilter.js'
29
+ import { transformFlatten, transformFlattenIfNeeded } from './transform/transformFlatten.js'
30
+ import { transformLimit } from './transform/transformLimit.js'
31
+ import {
32
+ transformLogProgress,
33
+ type TransformLogProgressOptions,
34
+ } from './transform/transformLogProgress.js'
35
+ import { transformMap, type TransformMapOptions } from './transform/transformMap.js'
36
+ import {
37
+ transformMapSimple,
38
+ type TransformMapSimpleOptions,
39
+ } from './transform/transformMapSimple.js'
40
+ import { transformMapSync, type TransformMapSyncOptions } from './transform/transformMapSync.js'
41
+ import { transformOffset, type TransformOffsetOptions } from './transform/transformOffset.js'
42
+ import { transformTap, type TransformTapOptions } from './transform/transformTap.js'
43
+ import { transformThrottle, type TransformThrottleOptions } from './transform/transformThrottle.js'
44
+ import { writablePushToArray } from './writable/writablePushToArray.js'
45
+ import { writableVoid } from './writable/writableVoid.js'
46
+
47
+ export class Pipeline<T> {
48
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ok
49
+ private readonly source: Readable
50
+ private transforms: NodeJS.ReadWriteStream[] = []
51
+ private destination?: NodeJS.WritableStream
52
+ private readableLimit?: Integer
53
+ private abortableSignal = createAbortableSignal()
54
+
55
+ private constructor(source: ReadableTyped<T>) {
56
+ this.source = source
57
+ }
58
+
59
+ static from<T>(source: ReadableTyped<T>): Pipeline<T> {
60
+ return new Pipeline(source)
61
+ }
62
+
63
+ /**
64
+ * Technically same as `fromIterable` (since Array is Iterable),
65
+ * but named a bit friendlier.
66
+ */
67
+ static fromArray<T>(input: T[]): Pipeline<T> {
68
+ return new Pipeline(Readable.from(input))
69
+ }
70
+
71
+ static fromIterable<T>(input: Iterable<T> | AsyncIterable<T>): Pipeline<T> {
72
+ return new Pipeline(Readable.from(input))
73
+ }
74
+
75
+ static fromNDJsonFile<T>(sourceFilePath: string): Pipeline<T> {
76
+ return new Pipeline(createReadStreamAsNDJSON<T>(sourceFilePath))
77
+ }
78
+
79
+ /**
80
+ * Limits the source Readable, but using `.take(limit)` on it.
81
+ * This is THE preferred way of limiting the source.
82
+ */
83
+ limitSource(limit: NonNegativeInteger | undefined): this {
84
+ this.readableLimit = limit
85
+ return this
86
+ }
87
+
88
+ /**
89
+ * If possible - STRONGLY PREFER applying `.take(limit)` on the source Readable,
90
+ * as it's a clean graceful way of limiting the Readable. Example:
91
+ *
92
+ * Pipeline.from(myReadable.take(10))
93
+ *
94
+ * or
95
+ *
96
+ * Pipeline
97
+ * .from(myReadable)
98
+ * .limitSource(10)
99
+ *
100
+ * If applying `take` on Readable is not possible - use this method at your own risk.
101
+ * Why warning?
102
+ * The limit works by aborting the stream, and then catching the error - certainly
103
+ * less clean than `.take()` on the source.
104
+ */
105
+ limit(limit: NonNegativeInteger | undefined): this {
106
+ this.transforms.push(
107
+ transformLimit({
108
+ limit,
109
+ signal: this.abortableSignal,
110
+ }),
111
+ )
112
+ return this
113
+ }
114
+
115
+ chunk(chunkSize: PositiveInteger, opt?: TransformOptions): Pipeline<T[]> {
116
+ this.transforms.push(transformChunk(chunkSize, opt))
117
+ return this as any
118
+ }
119
+
120
+ flatten<TO>(this: Pipeline<readonly TO[]>): Pipeline<TO> {
121
+ this.transforms.push(transformFlatten())
122
+ return this as any
123
+ }
124
+
125
+ flattenIfNeeded(): Pipeline<T extends readonly (infer TO)[] ? TO : T> {
126
+ this.transforms.push(transformFlattenIfNeeded())
127
+ return this as any
128
+ }
129
+
130
+ // TransformLogProgressOptions intentionally doesn't have <T> passed, as it's inconvenient in many cases
131
+ logProgress(opt?: TransformLogProgressOptions): this {
132
+ this.transforms.push(transformLogProgress(opt))
133
+ return this
134
+ }
135
+
136
+ map<TO>(
137
+ mapper: AbortableAsyncMapper<T, TO | typeof SKIP | typeof END>,
138
+ opt?: TransformMapOptions<T, TO>,
139
+ ): Pipeline<TO> {
140
+ this.transforms.push(
141
+ transformMap(mapper, {
142
+ ...opt,
143
+ signal: this.abortableSignal,
144
+ }),
145
+ )
146
+ return this as any
147
+ }
148
+
149
+ mapSync<TO>(
150
+ mapper: IndexedMapper<T, TO | typeof SKIP | typeof END>,
151
+ opt?: TransformMapSyncOptions,
152
+ ): Pipeline<TO> {
153
+ this.transforms.push(
154
+ transformMapSync(mapper, {
155
+ ...opt,
156
+ signal: this.abortableSignal,
157
+ }),
158
+ )
159
+ return this as any
160
+ }
161
+
162
+ mapSimple<TO>(mapper: IndexedMapper<T, TO>, opt?: TransformMapSimpleOptions): Pipeline<TO> {
163
+ this.transforms.push(transformMapSimple(mapper, opt))
164
+ return this as any
165
+ }
166
+
167
+ filter(predicate: AsyncPredicate<T>, opt?: TransformMapOptions): this {
168
+ this.transforms.push(
169
+ transformMap(v => v, {
170
+ predicate,
171
+ ...opt,
172
+ signal: this.abortableSignal,
173
+ }),
174
+ )
175
+ return this
176
+ }
177
+
178
+ filterSync(predicate: Predicate<T>, opt?: TransformOptions): this {
179
+ this.transforms.push(transformFilterSync(predicate, opt))
180
+ return this
181
+ }
182
+
183
+ offset(opt: TransformOffsetOptions): this {
184
+ this.transforms.push(transformOffset(opt))
185
+ return this
186
+ }
187
+
188
+ tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformTapOptions): this {
189
+ this.transforms.push(transformTap(fn, opt))
190
+ return this
191
+ }
192
+
193
+ throttle(opt: TransformThrottleOptions): this {
194
+ this.transforms.push(transformThrottle(opt))
195
+ return this
196
+ }
197
+
198
+ // todo: tee/fork
199
+
200
+ transform<TO>(transform: TransformTyped<T, TO>): Pipeline<TO> {
201
+ this.transforms.push(transform)
202
+ return this as any
203
+ }
204
+
205
+ /**
206
+ * Helper method to add multiple transforms at once.
207
+ * Not type safe! Prefer using singular `transform()` multiple times for type safety.
208
+ */
209
+ transformMany<TO>(transforms: Transform[]): Pipeline<TO> {
210
+ this.transforms.push(...transforms)
211
+ return this as any
212
+ }
213
+
214
+ /**
215
+ * Utility method just to conveniently type-cast the current Pipeline type.
216
+ * No runtime effect.
217
+ */
218
+ typeCastAs<TO>(): Pipeline<TO> {
219
+ return this as any
220
+ }
221
+
222
+ async toArray(opt?: TransformOptions): Promise<T[]> {
223
+ const arr: T[] = []
224
+ this.destination = writablePushToArray(arr, opt)
225
+ await this.run()
226
+ return arr
227
+ }
228
+
229
+ async toFile(outputFilePath: string): Promise<void> {
230
+ fs2.ensureFile(outputFilePath)
231
+ this.destination = fs2.createWriteStream(outputFilePath)
232
+ await this.run()
233
+ }
234
+
235
+ async toNDJsonFile(outputFilePath: string): Promise<void> {
236
+ fs2.ensureFile(outputFilePath)
237
+ this.transforms.push(transformToNDJson())
238
+ if (outputFilePath.endsWith('.gz')) {
239
+ this.transforms.push(
240
+ createGzip({
241
+ // chunkSize: 64 * 1024, // no observed speedup
242
+ }),
243
+ )
244
+ }
245
+ this.destination = fs2.createWriteStream(outputFilePath, {
246
+ // highWaterMark: 64 * 1024, // no observed speedup
247
+ })
248
+ await this.run()
249
+ }
250
+
251
+ async to(destination: WritableTyped<T>): Promise<void> {
252
+ this.destination = destination
253
+ await this.run()
254
+ }
255
+
256
+ async forEach(
257
+ fn: AsyncIndexedMapper<T, void>,
258
+ opt?: TransformMapOptions<T, void>,
259
+ ): Promise<void> {
260
+ this.transforms.push(
261
+ transformMap(fn, {
262
+ ...opt,
263
+ signal: this.abortableSignal,
264
+ }),
265
+ )
266
+ await this.run()
267
+ }
268
+
269
+ async forEachSync(
270
+ fn: IndexedMapper<T, void>,
271
+ opt?: TransformMapSyncOptions<T, void>,
272
+ ): Promise<void> {
273
+ this.transforms.push(
274
+ transformMapSync(fn, {
275
+ ...opt,
276
+ signal: this.abortableSignal,
277
+ }),
278
+ )
279
+ await this.run()
280
+ }
281
+
282
+ async run(): Promise<void> {
283
+ this.destination ||= writableVoid()
284
+ let { source } = this
285
+ if (this.readableLimit) {
286
+ source = source.take(this.readableLimit)
287
+ }
288
+
289
+ try {
290
+ await pipeline([source, ...this.transforms, this.destination], {
291
+ signal: this.abortableSignal,
292
+ })
293
+ } catch (err) {
294
+ if (err instanceof Error && (err.cause as any)?.message === PIPELINE_GRACEFUL_ABORT) {
295
+ console.log('pipeline gracefully aborted') // todo: this message may be removed later
296
+ return
297
+ }
298
+ throw err
299
+ }
300
+ }
301
+ }
@@ -1,29 +1 @@
1
- import type { Readable } from 'node:stream'
2
- import type { CommonLogger } from '@naturalcycles/js-lib/log'
3
-
4
- export function pipelineClose(
5
- name: string,
6
- readableDownstream: Readable,
7
- sourceReadable: Readable | undefined,
8
- streamDone: Promise<void> | undefined,
9
- logger: CommonLogger,
10
- ): void {
11
- readableDownstream.push(null) // this closes the stream, so downstream Readable will receive `end` and won't write anything
12
-
13
- if (!sourceReadable) {
14
- logger.warn(`${name} sourceReadable is not provided, readable stream will not be stopped`)
15
- } else {
16
- logger.log(`${name} is calling readable.unpipe() to pause the stream`)
17
- sourceReadable.unpipe() // it is expected to pause the stream
18
-
19
- if (!streamDone) {
20
- logger.log(`${name} streamDone is not provided, will do readable.destroy right away`)
21
- sourceReadable.destroy()
22
- } else {
23
- void streamDone.then(() => {
24
- logger.log(`${name} streamDone, calling readable.destroy()`)
25
- sourceReadable.destroy() // this throws ERR_STREAM_PREMATURE_CLOSE
26
- })
27
- }
28
- }
29
- }
1
+ export const PIPELINE_GRACEFUL_ABORT = 'PIPELINE_GRACEFUL_ABORT'