@naturalcycles/nodejs-lib 15.21.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 (50) hide show
  1. package/dist/exec2/exec2.js +1 -0
  2. package/dist/stream/index.d.ts +2 -2
  3. package/dist/stream/index.js +2 -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/ndjson/transformJsonParse.js +0 -1
  9. package/dist/stream/pipeline.d.ts +79 -0
  10. package/dist/stream/pipeline.js +220 -0
  11. package/dist/stream/readable/readableCombined.d.ts +30 -0
  12. package/dist/stream/readable/readableCombined.js +77 -0
  13. package/dist/stream/stream.util.d.ts +1 -3
  14. package/dist/stream/stream.util.js +1 -20
  15. package/dist/stream/transform/transformChunk.d.ts +5 -8
  16. package/dist/stream/transform/transformChunk.js +4 -2
  17. package/dist/stream/transform/transformFlatten.d.ts +1 -0
  18. package/dist/stream/transform/transformFlatten.js +15 -4
  19. package/dist/stream/transform/transformLimit.d.ts +3 -26
  20. package/dist/stream/transform/transformLimit.js +14 -23
  21. package/dist/stream/transform/transformMap.d.ts +15 -2
  22. package/dist/stream/transform/transformMap.js +25 -19
  23. package/dist/stream/transform/transformMapSync.d.ts +5 -3
  24. package/dist/stream/transform/transformMapSync.js +7 -8
  25. package/dist/stream/transform/transformTee.js +4 -2
  26. package/dist/stream/writable/writableForEach.d.ts +2 -1
  27. package/dist/stream/writable/writableFork.js +2 -2
  28. package/package.json +1 -1
  29. package/src/exec2/exec2.ts +1 -0
  30. package/src/stream/index.ts +2 -2
  31. package/src/stream/ndjson/ndjsonMap.ts +12 -22
  32. package/src/stream/ndjson/ndjsonStreamForEach.ts +8 -15
  33. package/src/stream/ndjson/transformJsonParse.ts +0 -1
  34. package/src/stream/pipeline.ts +301 -0
  35. package/src/stream/readable/readableCombined.ts +87 -0
  36. package/src/stream/stream.util.ts +1 -29
  37. package/src/stream/transform/transformChunk.ts +8 -11
  38. package/src/stream/transform/transformFlatten.ts +16 -4
  39. package/src/stream/transform/transformLimit.ts +20 -51
  40. package/src/stream/transform/transformMap.ts +45 -21
  41. package/src/stream/transform/transformMapSync.ts +14 -8
  42. package/src/stream/transform/transformTee.ts +5 -2
  43. package/src/stream/writable/writableForEach.ts +2 -2
  44. package/src/stream/writable/writableFork.ts +2 -2
  45. package/dist/stream/pipeline/pipeline.d.ts +0 -36
  46. package/dist/stream/pipeline/pipeline.js +0 -82
  47. package/dist/stream/readable/readableForEach.d.ts +0 -19
  48. package/dist/stream/readable/readableForEach.js +0 -30
  49. package/src/stream/pipeline/pipeline.ts +0 -114
  50. package/src/stream/readable/readableForEach.ts +0 -42
@@ -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
+ }
@@ -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
+ }
@@ -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'
@@ -1,22 +1,19 @@
1
1
  import { Transform } from 'node:stream'
2
+ import type { PositiveInteger } from '@naturalcycles/js-lib/types'
2
3
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
3
4
 
4
- export interface TransformChunkOptions extends TransformOptions {
5
- /**
6
- * How many items to include in each chunk.
7
- * Last chunk will contain the remaining items, possibly less than chunkSize.
8
- */
9
- chunkSize: number
10
- }
11
-
12
5
  /**
13
6
  * Similar to RxJS bufferCount(),
14
7
  * allows to "chunk" the input stream into chunks of `opt.chunkSize` size.
15
8
  * Last chunk will contain the remaining items, possibly less than chunkSize.
9
+ *
10
+ * `chunkSize` indicates how many items to include in each chunk.
11
+ * Last chunk will contain the remaining items, possibly less than chunkSize.
16
12
  */
17
- export function transformChunk<IN = any>(opt: TransformChunkOptions): TransformTyped<IN, IN[]> {
18
- const { chunkSize } = opt
19
-
13
+ export function transformChunk<IN = any>(
14
+ chunkSize: PositiveInteger,
15
+ opt?: TransformOptions,
16
+ ): TransformTyped<IN, IN[]> {
20
17
  let buf: IN[] = []
21
18
 
22
19
  return new Transform({
@@ -5,13 +5,25 @@ export function transformFlatten<T>(): TransformTyped<T[], T> {
5
5
  return new Transform({
6
6
  objectMode: true,
7
7
  transform(chunk: T[], _, cb) {
8
- if (!Array.isArray(chunk)) {
9
- // As a safety precaution, to not crash the pipeline - push as is
10
- this.push(chunk)
11
- } else {
8
+ for (const item of chunk) {
9
+ this.push(item)
10
+ }
11
+ cb() // acknowledge
12
+ },
13
+ })
14
+ }
15
+
16
+ export function transformFlattenIfNeeded<T>(): TransformTyped<T[], T> {
17
+ return new Transform({
18
+ objectMode: true,
19
+ transform(chunk: T[], _, cb) {
20
+ if (Array.isArray(chunk)) {
12
21
  for (const item of chunk) {
13
22
  this.push(item)
14
23
  }
24
+ } else {
25
+ // As a safety precaution, to not crash the pipeline - push as is
26
+ this.push(chunk)
15
27
  }
16
28
  cb() // acknowledge
17
29
  },
@@ -1,8 +1,7 @@
1
- import type { Readable } from 'node:stream'
2
- import type { CommonLogger } from '@naturalcycles/js-lib/log'
3
- import { AbortableTransform } from '../pipeline/pipeline.js'
1
+ import { Transform } from 'node:stream'
2
+ import type { AbortableSignal } from '@naturalcycles/js-lib'
4
3
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
5
- import { pipelineClose } from '../stream.util.js'
4
+ import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js'
6
5
  import { transformNoOp } from './transformNoOp.js'
7
6
 
8
7
  export interface TransformLimitOptions extends TransformOptions {
@@ -12,72 +11,42 @@ export interface TransformLimitOptions extends TransformOptions {
12
11
  limit?: number
13
12
 
14
13
  /**
15
- * If provided (recommended!) - it will call readable.destroy() on limit.
16
- * Without it - it will only stop the downstream consumers, but won't stop
17
- * the Readable ("source" of the stream).
18
- * It is almost always crucial to stop the Source too, so, please provide the Readable here!
14
+ * Allows to abort (gracefully stop) the stream from inside the Transform.
19
15
  */
20
- sourceReadable?: Readable
21
-
22
- /**
23
- * Please provide it (a Promise that resolves when the Stream is done, e.g finished consuming things)
24
- * to be able to wait for Consumers before calling `readable.destroy`.
25
- * Has no effect if `readable` is not provided.
26
- */
27
- streamDone?: Promise<void>
28
-
29
- logger?: CommonLogger
30
-
31
- /**
32
- * Set to true to enable additional debug messages, e.g it'll log
33
- * when readable still emits values after the limit is reached.
34
- */
35
- debug?: boolean
16
+ signal: AbortableSignal
36
17
  }
37
18
 
38
- /**
39
- * Class only exists to be able to do `instanceof TransformLimit`
40
- * and to set sourceReadable+streamDone to it in `_pipeline`.
41
- */
42
- export class TransformLimit extends AbortableTransform {}
43
-
44
19
  export function transformLimit<IN>(opt: TransformLimitOptions): TransformTyped<IN, IN> {
45
- const { logger = console, limit, debug } = opt
20
+ const { limit, signal } = opt
46
21
 
47
22
  if (!limit) {
48
- // No limit - returning pass-through transform
49
23
  return transformNoOp()
50
24
  }
51
25
 
52
26
  let i = 0 // so we start first chunk with 1
53
27
  let ended = false
54
- return new TransformLimit({
28
+ return new Transform({
55
29
  objectMode: true,
56
30
  ...opt,
57
- transform(this: TransformLimit, chunk, _, cb) {
31
+ transform(chunk, _, cb) {
32
+ if (ended) {
33
+ return
34
+ }
35
+
58
36
  i++
59
37
 
60
38
  if (i === limit) {
61
39
  ended = true
62
- logger.log(`transformLimit of ${limit} reached`)
63
40
  this.push(chunk)
64
-
65
- pipelineClose(
66
- 'transformLimit',
67
- this,
68
- opt.sourceReadable || this.sourceReadable,
69
- opt.streamDone || this.streamDone,
70
- logger,
71
- )
72
-
73
- cb() // after pause
74
- } else if (!ended) {
75
- cb(null, chunk)
76
- } else {
77
- if (debug) logger.log(`transformLimit.transform after limit`, i)
78
- // If we ever HANG (don't call cb) - Node will do process.exit(0) to us
79
- cb() // ended, don't emit anything
41
+ this.push(null) // tell downstream that we're done
42
+ cb()
43
+ queueMicrotask(() => {
44
+ signal.abort(new Error(PIPELINE_GRACEFUL_ABORT))
45
+ })
46
+ return
80
47
  }
48
+
49
+ cb(null, chunk)
81
50
  },
82
51
  })
83
52
  }