@naturalcycles/nodejs-lib 15.125.2 → 15.125.3

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.
@@ -116,4 +116,5 @@ export declare class Pipeline<T = unknown> {
116
116
  forEach(fn: AbortableAsyncMapper<T, void>, opt?: TransformMapOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
117
117
  forEachSync(fn: AbortableMapper<T, void>, opt?: TransformMapSyncOptions<T, void> & TransformLogProgressOptions<T>): Promise<void>;
118
118
  run(): Promise<void>;
119
+ private isGracefullyAborted;
119
120
  }
@@ -347,11 +347,18 @@ export class Pipeline {
347
347
  });
348
348
  }
349
349
  catch (err) {
350
- if (err instanceof Error && err.cause?.message === PIPELINE_GRACEFUL_ABORT) {
350
+ // The abort reason is checked on the signal rather than on `err.cause`: when the source has
351
+ // already emitted `end` by the time the abort lands (small `Readable.from` sources), node's
352
+ // pipeline reports ERR_STREAM_PREMATURE_CLOSE without a cause instead of the AbortError.
353
+ if (this.isGracefullyAborted()) {
351
354
  console.log('pipeline gracefully aborted'); // todo: this message may be removed later
352
355
  return;
353
356
  }
354
357
  throw err;
355
358
  }
356
359
  }
360
+ isGracefullyAborted() {
361
+ const { aborted, reason } = this.abortableSignal;
362
+ return aborted && reason instanceof Error && reason.message === PIPELINE_GRACEFUL_ABORT;
363
+ }
357
364
  }
@@ -31,6 +31,7 @@ export function transformMap(mapper, opt = {}) {
31
31
  let inFlight = 0;
32
32
  let blockedCallback = null;
33
33
  let flushBlocked = null;
34
+ let destroyBlocked = null;
34
35
  // Warmup - cached concurrency to reduce Date.now() syscalls
35
36
  let warmupComplete = warmupSeconds <= 0 || maxConcurrency <= 1;
36
37
  let concurrency = warmupComplete ? maxConcurrency : 1;
@@ -117,7 +118,20 @@ export function transformMap(mapper, opt = {}) {
117
118
  if (inFlight === 0 && flushBlocked) {
118
119
  flushBlocked.resolve();
119
120
  }
121
+ // Let a pending destroy complete once nothing is in flight any more
122
+ if (inFlight === 0 && destroyBlocked) {
123
+ destroyBlocked.resolve();
124
+ }
125
+ }
126
+ },
127
+ async destroy(err, cb) {
128
+ // An upstream abort (e.g. `limit` or END) destroys this stream while mappers are still running.
129
+ // The close is held until they settle, so that the pipeline resolves only once their side effects are done.
130
+ if (inFlight > 0) {
131
+ destroyBlocked = pDefer();
132
+ await destroyBlocked;
120
133
  }
134
+ cb(err);
121
135
  },
122
136
  async flush(cb) {
123
137
  // Wait for all in-flight operations to complete
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.125.2",
4
+ "version": "15.125.3",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -14,7 +14,7 @@
14
14
  "yaml": "^2"
15
15
  },
16
16
  "devDependencies": {
17
- "@naturalcycles/dev-lib": "0.0.0",
17
+ "@naturalcycles/dev-lib": "20.65.1",
18
18
  "typescript": "^7"
19
19
  },
20
20
  "exports": {
@@ -466,11 +466,19 @@ export class Pipeline<T = unknown> {
466
466
  signal: this.abortableSignal,
467
467
  })
468
468
  } catch (err) {
469
- if (err instanceof Error && (err.cause as any)?.message === PIPELINE_GRACEFUL_ABORT) {
469
+ // The abort reason is checked on the signal rather than on `err.cause`: when the source has
470
+ // already emitted `end` by the time the abort lands (small `Readable.from` sources), node's
471
+ // pipeline reports ERR_STREAM_PREMATURE_CLOSE without a cause instead of the AbortError.
472
+ if (this.isGracefullyAborted()) {
470
473
  console.log('pipeline gracefully aborted') // todo: this message may be removed later
471
474
  return
472
475
  }
473
476
  throw err
474
477
  }
475
478
  }
479
+
480
+ private isGracefullyAborted(): boolean {
481
+ const { aborted, reason } = this.abortableSignal
482
+ return aborted && reason instanceof Error && reason.message === PIPELINE_GRACEFUL_ABORT
483
+ }
476
484
  }
@@ -127,6 +127,7 @@ export function transformMap<IN = any, OUT = IN>(
127
127
  let inFlight = 0
128
128
  let blockedCallback: (() => void) | null = null
129
129
  let flushBlocked: DeferredPromise | null = null
130
+ let destroyBlocked: DeferredPromise | null = null
130
131
 
131
132
  // Warmup - cached concurrency to reduce Date.now() syscalls
132
133
  let warmupComplete = warmupSeconds <= 0 || maxConcurrency <= 1
@@ -220,7 +221,22 @@ export function transformMap<IN = any, OUT = IN>(
220
221
  if (inFlight === 0 && flushBlocked) {
221
222
  flushBlocked.resolve()
222
223
  }
224
+
225
+ // Let a pending destroy complete once nothing is in flight any more
226
+ if (inFlight === 0 && destroyBlocked) {
227
+ destroyBlocked.resolve()
228
+ }
229
+ }
230
+ },
231
+ async destroy(err, cb) {
232
+ // An upstream abort (e.g. `limit` or END) destroys this stream while mappers are still running.
233
+ // The close is held until they settle, so that the pipeline resolves only once their side effects are done.
234
+ if (inFlight > 0) {
235
+ destroyBlocked = pDefer()
236
+ await destroyBlocked
223
237
  }
238
+
239
+ cb(err)
224
240
  },
225
241
  async flush(cb) {
226
242
  // Wait for all in-flight operations to complete