@naturalcycles/nodejs-lib 15.70.1 → 15.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,66 +0,0 @@
1
- import type { AbortableSignal } from '@naturalcycles/js-lib';
2
- import { ErrorMode } from '@naturalcycles/js-lib/error';
3
- import { type AbortableAsyncMapper, type AsyncPredicate, END, type NumberOfSeconds, type PositiveInteger, type Predicate, type Promisable, SKIP } from '@naturalcycles/js-lib/types';
4
- import type { TransformOptions, TransformTyped } from '../stream.model.js';
5
- import type { TransformMapStats } from './transformMap.js';
6
- export interface TransformMap2Options<IN = any, OUT = IN> extends TransformOptions {
7
- /**
8
- * Predicate to filter outgoing results (after mapper).
9
- * Allows to not emit all results.
10
- *
11
- * Defaults to "pass everything" (including null, undefined, etc).
12
- * Simpler way to exclude certain cases is to return SKIP symbol from the mapper.
13
- */
14
- predicate?: Predicate<OUT>;
15
- asyncPredicate?: AsyncPredicate<OUT>;
16
- /**
17
- * Number of concurrently pending promises returned by `mapper`.
18
- *
19
- * @default 16
20
- */
21
- concurrency?: PositiveInteger;
22
- /**
23
- * Time in seconds to gradually increase concurrency from 1 to `concurrency`.
24
- * Useful for warming up connections to databases, APIs, etc.
25
- *
26
- * Set to 0 to disable warmup (default).
27
- */
28
- warmupSeconds?: NumberOfSeconds;
29
- /**
30
- * @default THROW_IMMEDIATELY
31
- */
32
- errorMode?: ErrorMode;
33
- /**
34
- * If defined - will be called on every error happening in the stream.
35
- * Called BEFORE observable will emit error (unless skipErrors is set to true).
36
- */
37
- onError?: (err: Error, input: IN) => any;
38
- /**
39
- * A hook that is called when the last item is finished processing.
40
- * stats object is passed, containing countIn and countOut -
41
- * number of items that entered the transform and number of items that left it.
42
- *
43
- * Callback is called **before** [possible] Aggregated error is thrown,
44
- * and before [possible] THROW_IMMEDIATELY error.
45
- *
46
- * onDone callback will be awaited before Error is thrown.
47
- */
48
- onDone?: (stats: TransformMapStats) => Promisable<any>;
49
- /**
50
- * Progress metric
51
- *
52
- * @default `stream`
53
- */
54
- metric?: string;
55
- /**
56
- * Allows to abort (gracefully stop) the stream from inside the Transform.
57
- */
58
- signal?: AbortableSignal;
59
- }
60
- /**
61
- * Like transformMap, but with native concurrency control (no through2-concurrent dependency)
62
- * and support for gradual warmup.
63
- *
64
- * @experimental
65
- */
66
- export declare function transformMap2<IN = any, OUT = IN>(mapper: AbortableAsyncMapper<IN, OUT | typeof SKIP | typeof END>, opt?: TransformMap2Options<IN, OUT>): TransformTyped<IN, OUT>;
@@ -1,171 +0,0 @@
1
- import { Transform } from 'node:stream';
2
- import { _since } from '@naturalcycles/js-lib/datetime';
3
- import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
4
- import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
5
- import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
6
- import { END, SKIP, } from '@naturalcycles/js-lib/types';
7
- import { yellow } from '../../colors/colors.js';
8
- import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
9
- const WARMUP_CHECK_INTERVAL_MS = 1000;
10
- /**
11
- * Like transformMap, but with native concurrency control (no through2-concurrent dependency)
12
- * and support for gradual warmup.
13
- *
14
- * @experimental
15
- */
16
- export function transformMap2(mapper, opt = {}) {
17
- const { concurrency: maxConcurrency = 16, warmupSeconds = 0, predicate, asyncPredicate, errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', signal, objectMode = true, highWaterMark = 64, } = opt;
18
- const warmupMs = warmupSeconds * 1000;
19
- const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
20
- // Stats
21
- let started = 0;
22
- let index = -1;
23
- let countOut = 0;
24
- let isSettled = false;
25
- let ok = true;
26
- let errors = 0;
27
- const collectedErrors = [];
28
- // Concurrency control - single counter, single callback for backpressure
29
- let inFlight = 0;
30
- let blockedCallback = null;
31
- let flushBlocked = null;
32
- // Warmup - cached concurrency to reduce Date.now() syscalls
33
- let warmupComplete = warmupSeconds <= 0 || maxConcurrency <= 1;
34
- let concurrency = warmupComplete ? maxConcurrency : 1;
35
- let lastWarmupCheck = 0;
36
- return new Transform({
37
- objectMode,
38
- readableHighWaterMark: highWaterMark,
39
- writableHighWaterMark: highWaterMark,
40
- async transform(chunk, _, cb) {
41
- // Initialize start time on first item
42
- if (started === 0) {
43
- started = Date.now();
44
- lastWarmupCheck = started;
45
- }
46
- if (isSettled)
47
- return cb();
48
- const currentIndex = ++index;
49
- inFlight++;
50
- if (!warmupComplete) {
51
- updateConcurrency();
52
- }
53
- // Apply backpressure if at capacity, otherwise request more input
54
- if (inFlight < concurrency) {
55
- cb();
56
- }
57
- else {
58
- blockedCallback = cb;
59
- }
60
- try {
61
- const res = await mapper(chunk, currentIndex);
62
- if (isSettled)
63
- return;
64
- if (res === END) {
65
- isSettled = true;
66
- logger.log(`transformMap2 END received at index ${currentIndex}`);
67
- _assert(signal, 'signal is required when using END');
68
- signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
69
- return;
70
- }
71
- if (res === SKIP)
72
- return;
73
- let shouldPush = true;
74
- if (predicate) {
75
- shouldPush = predicate(res, currentIndex);
76
- }
77
- else if (asyncPredicate) {
78
- shouldPush = (await asyncPredicate(res, currentIndex)) && !isSettled;
79
- }
80
- if (shouldPush) {
81
- countOut++;
82
- this.push(res);
83
- }
84
- }
85
- catch (err) {
86
- logger.error(err);
87
- errors++;
88
- logErrorStats();
89
- if (onError) {
90
- try {
91
- onError(_anyToError(err), chunk);
92
- }
93
- catch { }
94
- }
95
- if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
96
- isSettled = true;
97
- ok = false;
98
- await callOnDone();
99
- this.destroy(_anyToError(err));
100
- return;
101
- }
102
- if (errorMode === ErrorMode.THROW_AGGREGATED) {
103
- collectedErrors.push(_anyToError(err));
104
- }
105
- }
106
- finally {
107
- inFlight--;
108
- // Release blocked callback if we now have capacity
109
- if (blockedCallback && inFlight < concurrency) {
110
- const pendingCb = blockedCallback;
111
- blockedCallback = null;
112
- pendingCb();
113
- }
114
- // Trigger flush completion if all done
115
- if (inFlight === 0 && flushBlocked) {
116
- flushBlocked.resolve();
117
- }
118
- }
119
- },
120
- async flush(cb) {
121
- // Wait for all in-flight operations to complete
122
- if (inFlight > 0) {
123
- flushBlocked = pDefer();
124
- await flushBlocked;
125
- }
126
- logErrorStats(true);
127
- await callOnDone();
128
- if (collectedErrors.length) {
129
- cb(new AggregateError(collectedErrors, `transformMap2 resulted in ${collectedErrors.length} error(s)`));
130
- }
131
- else {
132
- cb();
133
- }
134
- },
135
- });
136
- function updateConcurrency() {
137
- const now = Date.now();
138
- if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS)
139
- return;
140
- lastWarmupCheck = now;
141
- const elapsed = now - started;
142
- if (elapsed >= warmupMs) {
143
- warmupComplete = true;
144
- concurrency = maxConcurrency;
145
- logger.log(`transformMap2: warmup complete in ${_since(started)}`);
146
- return;
147
- }
148
- const progress = elapsed / warmupMs;
149
- concurrency = Math.max(1, Math.floor(1 + (maxConcurrency - 1) * progress));
150
- }
151
- function logErrorStats(final = false) {
152
- if (!errors)
153
- return;
154
- logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`);
155
- }
156
- async function callOnDone() {
157
- try {
158
- await onDone?.({
159
- ok: collectedErrors.length === 0 && ok,
160
- collectedErrors,
161
- countErrors: errors,
162
- countIn: index + 1,
163
- countOut,
164
- started,
165
- });
166
- }
167
- catch (err) {
168
- logger.error(err);
169
- }
170
- }
171
- }
@@ -1,283 +0,0 @@
1
- import { Transform } from 'node:stream'
2
- import type { AbortableSignal } from '@naturalcycles/js-lib'
3
- import { _since } from '@naturalcycles/js-lib/datetime'
4
- import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
5
- import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
6
- import type { DeferredPromise } from '@naturalcycles/js-lib/promise'
7
- import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
8
- import {
9
- type AbortableAsyncMapper,
10
- type AsyncPredicate,
11
- END,
12
- type NumberOfSeconds,
13
- type PositiveInteger,
14
- type Predicate,
15
- type Promisable,
16
- SKIP,
17
- type UnixTimestampMillis,
18
- } from '@naturalcycles/js-lib/types'
19
- import { yellow } from '../../colors/colors.js'
20
- import type { TransformOptions, TransformTyped } from '../stream.model.js'
21
- import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js'
22
- import type { TransformMapStats } from './transformMap.js'
23
-
24
- export interface TransformMap2Options<IN = any, OUT = IN> extends TransformOptions {
25
- /**
26
- * Predicate to filter outgoing results (after mapper).
27
- * Allows to not emit all results.
28
- *
29
- * Defaults to "pass everything" (including null, undefined, etc).
30
- * Simpler way to exclude certain cases is to return SKIP symbol from the mapper.
31
- */
32
- predicate?: Predicate<OUT>
33
-
34
- asyncPredicate?: AsyncPredicate<OUT>
35
-
36
- /**
37
- * Number of concurrently pending promises returned by `mapper`.
38
- *
39
- * @default 16
40
- */
41
- concurrency?: PositiveInteger
42
-
43
- /**
44
- * Time in seconds to gradually increase concurrency from 1 to `concurrency`.
45
- * Useful for warming up connections to databases, APIs, etc.
46
- *
47
- * Set to 0 to disable warmup (default).
48
- */
49
- warmupSeconds?: NumberOfSeconds
50
-
51
- /**
52
- * @default THROW_IMMEDIATELY
53
- */
54
- errorMode?: ErrorMode
55
-
56
- /**
57
- * If defined - will be called on every error happening in the stream.
58
- * Called BEFORE observable will emit error (unless skipErrors is set to true).
59
- */
60
- onError?: (err: Error, input: IN) => any
61
-
62
- /**
63
- * A hook that is called when the last item is finished processing.
64
- * stats object is passed, containing countIn and countOut -
65
- * number of items that entered the transform and number of items that left it.
66
- *
67
- * Callback is called **before** [possible] Aggregated error is thrown,
68
- * and before [possible] THROW_IMMEDIATELY error.
69
- *
70
- * onDone callback will be awaited before Error is thrown.
71
- */
72
- onDone?: (stats: TransformMapStats) => Promisable<any>
73
-
74
- /**
75
- * Progress metric
76
- *
77
- * @default `stream`
78
- */
79
- metric?: string
80
-
81
- /**
82
- * Allows to abort (gracefully stop) the stream from inside the Transform.
83
- */
84
- signal?: AbortableSignal
85
- }
86
-
87
- const WARMUP_CHECK_INTERVAL_MS = 1000
88
-
89
- /**
90
- * Like transformMap, but with native concurrency control (no through2-concurrent dependency)
91
- * and support for gradual warmup.
92
- *
93
- * @experimental
94
- */
95
- export function transformMap2<IN = any, OUT = IN>(
96
- mapper: AbortableAsyncMapper<IN, OUT | typeof SKIP | typeof END>,
97
- opt: TransformMap2Options<IN, OUT> = {},
98
- ): TransformTyped<IN, OUT> {
99
- const {
100
- concurrency: maxConcurrency = 16,
101
- warmupSeconds = 0,
102
- predicate,
103
- asyncPredicate,
104
- errorMode = ErrorMode.THROW_IMMEDIATELY,
105
- onError,
106
- onDone,
107
- metric = 'stream',
108
- signal,
109
- objectMode = true,
110
- highWaterMark = 64,
111
- } = opt
112
-
113
- const warmupMs = warmupSeconds * 1000
114
- const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
115
-
116
- // Stats
117
- let started = 0 as UnixTimestampMillis
118
- let index = -1
119
- let countOut = 0
120
- let isSettled = false
121
- let ok = true
122
- let errors = 0
123
- const collectedErrors: Error[] = []
124
-
125
- // Concurrency control - single counter, single callback for backpressure
126
- let inFlight = 0
127
- let blockedCallback: (() => void) | null = null
128
- let flushBlocked: DeferredPromise | null = null
129
-
130
- // Warmup - cached concurrency to reduce Date.now() syscalls
131
- let warmupComplete = warmupSeconds <= 0 || maxConcurrency <= 1
132
- let concurrency = warmupComplete ? maxConcurrency : 1
133
- let lastWarmupCheck = 0
134
-
135
- return new Transform({
136
- objectMode,
137
- readableHighWaterMark: highWaterMark,
138
- writableHighWaterMark: highWaterMark,
139
- async transform(this: Transform, chunk: IN, _, cb) {
140
- // Initialize start time on first item
141
- if (started === 0) {
142
- started = Date.now() as UnixTimestampMillis
143
- lastWarmupCheck = started
144
- }
145
-
146
- if (isSettled) return cb()
147
-
148
- const currentIndex = ++index
149
- inFlight++
150
- if (!warmupComplete) {
151
- updateConcurrency()
152
- }
153
-
154
- // Apply backpressure if at capacity, otherwise request more input
155
- if (inFlight < concurrency) {
156
- cb()
157
- } else {
158
- blockedCallback = cb
159
- }
160
-
161
- try {
162
- const res: OUT | typeof SKIP | typeof END = await mapper(chunk, currentIndex)
163
-
164
- if (isSettled) return
165
-
166
- if (res === END) {
167
- isSettled = true
168
- logger.log(`transformMap2 END received at index ${currentIndex}`)
169
- _assert(signal, 'signal is required when using END')
170
- signal.abort(new Error(PIPELINE_GRACEFUL_ABORT))
171
- return
172
- }
173
-
174
- if (res === SKIP) return
175
-
176
- let shouldPush = true
177
- if (predicate) {
178
- shouldPush = predicate(res, currentIndex)
179
- } else if (asyncPredicate) {
180
- shouldPush = (await asyncPredicate(res, currentIndex)) && !isSettled
181
- }
182
-
183
- if (shouldPush) {
184
- countOut++
185
- this.push(res)
186
- }
187
- } catch (err) {
188
- logger.error(err)
189
- errors++
190
- logErrorStats()
191
-
192
- if (onError) {
193
- try {
194
- onError(_anyToError(err), chunk)
195
- } catch {}
196
- }
197
-
198
- if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
199
- isSettled = true
200
- ok = false
201
- await callOnDone()
202
- this.destroy(_anyToError(err))
203
- return
204
- }
205
- if (errorMode === ErrorMode.THROW_AGGREGATED) {
206
- collectedErrors.push(_anyToError(err))
207
- }
208
- } finally {
209
- inFlight--
210
-
211
- // Release blocked callback if we now have capacity
212
- if (blockedCallback && inFlight < concurrency) {
213
- const pendingCb = blockedCallback
214
- blockedCallback = null
215
- pendingCb()
216
- }
217
-
218
- // Trigger flush completion if all done
219
- if (inFlight === 0 && flushBlocked) {
220
- flushBlocked.resolve()
221
- }
222
- }
223
- },
224
- async flush(cb) {
225
- // Wait for all in-flight operations to complete
226
- if (inFlight > 0) {
227
- flushBlocked = pDefer()
228
- await flushBlocked
229
- }
230
-
231
- logErrorStats(true)
232
- await callOnDone()
233
-
234
- if (collectedErrors.length) {
235
- cb(
236
- new AggregateError(
237
- collectedErrors,
238
- `transformMap2 resulted in ${collectedErrors.length} error(s)`,
239
- ),
240
- )
241
- } else {
242
- cb()
243
- }
244
- },
245
- })
246
-
247
- function updateConcurrency(): void {
248
- const now = Date.now()
249
- if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS) return
250
- lastWarmupCheck = now
251
-
252
- const elapsed = now - started
253
- if (elapsed >= warmupMs) {
254
- warmupComplete = true
255
- concurrency = maxConcurrency
256
- logger.log(`transformMap2: warmup complete in ${_since(started)}`)
257
- return
258
- }
259
-
260
- const progress = elapsed / warmupMs
261
- concurrency = Math.max(1, Math.floor(1 + (maxConcurrency - 1) * progress))
262
- }
263
-
264
- function logErrorStats(final = false): void {
265
- if (!errors) return
266
- logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`)
267
- }
268
-
269
- async function callOnDone(): Promise<void> {
270
- try {
271
- await onDone?.({
272
- ok: collectedErrors.length === 0 && ok,
273
- collectedErrors,
274
- countErrors: errors,
275
- countIn: index + 1,
276
- countOut,
277
- started,
278
- })
279
- } catch (err) {
280
- logger.error(err)
281
- }
282
- }
283
- }