@naturalcycles/nodejs-lib 15.70.1 → 15.72.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.
- package/dist/stream/index.d.ts +0 -1
- package/dist/stream/index.js +0 -1
- package/dist/stream/pipeline.d.ts +13 -8
- package/dist/stream/pipeline.js +18 -32
- package/dist/stream/transform/transformFilter.js +2 -2
- package/dist/stream/transform/transformMap.d.ts +13 -25
- package/dist/stream/transform/transformMap.js +134 -118
- package/dist/stream/transform/worker/transformMultiThreaded.js +59 -39
- package/dist/zip/zip.util.d.ts +4 -1
- package/dist/zip/zip.util.js +14 -2
- package/package.json +1 -3
- package/src/stream/index.ts +0 -1
- package/src/stream/pipeline.ts +21 -48
- package/src/stream/transform/transformFilter.ts +2 -2
- package/src/stream/transform/transformMap.ts +168 -153
- package/src/stream/transform/worker/transformMultiThreaded.ts +57 -40
- package/src/stream/transform/worker/workerClassProxy.js +0 -4
- package/src/zip/zip.util.ts +15 -1
- package/dist/stream/transform/transformMap2.d.ts +0 -66
- package/dist/stream/transform/transformMap2.js +0 -171
- package/src/stream/transform/transformMap2.ts +0 -283
|
@@ -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
|
-
}
|