@naturalcycles/nodejs-lib 15.102.0 → 15.104.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 (35) hide show
  1. package/dist/stream/index.d.ts +1 -0
  2. package/dist/stream/index.js +1 -0
  3. package/dist/stream/pipeline.d.ts +2 -0
  4. package/dist/stream/pipeline.js +5 -0
  5. package/dist/stream/transform/transformChunk.js +1 -0
  6. package/dist/stream/transform/transformFilter.js +1 -0
  7. package/dist/stream/transform/transformFlatten.js +2 -0
  8. package/dist/stream/transform/transformFork.js +1 -1
  9. package/dist/stream/transform/transformLimit.js +1 -1
  10. package/dist/stream/transform/transformMapSimple.js +1 -1
  11. package/dist/stream/transform/transformMapSync.js +1 -0
  12. package/dist/stream/transform/transformNoOp.js +1 -0
  13. package/dist/stream/transform/transformOffset.js +1 -0
  14. package/dist/stream/transform/transformThrottle.js +1 -1
  15. package/dist/stream/transform/transformThrottleByRSS.d.ts +43 -0
  16. package/dist/stream/transform/transformThrottleByRSS.js +89 -0
  17. package/dist/stream/transform/transformWarmup.js +1 -1
  18. package/dist/zip/zip.util.d.ts +2 -0
  19. package/dist/zip/zip.util.js +9 -0
  20. package/package.json +1 -1
  21. package/src/stream/index.ts +1 -0
  22. package/src/stream/pipeline.ts +7 -0
  23. package/src/stream/transform/transformChunk.ts +1 -0
  24. package/src/stream/transform/transformFilter.ts +1 -0
  25. package/src/stream/transform/transformFlatten.ts +2 -0
  26. package/src/stream/transform/transformFork.ts +1 -1
  27. package/src/stream/transform/transformLimit.ts +1 -1
  28. package/src/stream/transform/transformMapSimple.ts +1 -1
  29. package/src/stream/transform/transformMapSync.ts +1 -0
  30. package/src/stream/transform/transformNoOp.ts +1 -0
  31. package/src/stream/transform/transformOffset.ts +1 -0
  32. package/src/stream/transform/transformThrottle.ts +1 -1
  33. package/src/stream/transform/transformThrottleByRSS.ts +150 -0
  34. package/src/stream/transform/transformWarmup.ts +1 -1
  35. package/src/zip/zip.util.ts +11 -0
@@ -23,6 +23,7 @@ export * from './transform/transformOffset.js';
23
23
  export * from './transform/transformSplit.js';
24
24
  export * from './transform/transformTap.js';
25
25
  export * from './transform/transformThrottle.js';
26
+ export * from './transform/transformThrottleByRSS.js';
26
27
  export * from './transform/transformWarmup.js';
27
28
  export * from './transform/worker/baseWorkerClass.js';
28
29
  export * from './transform/worker/transformMultiThreaded.js';
@@ -23,6 +23,7 @@ export * from './transform/transformOffset.js';
23
23
  export * from './transform/transformSplit.js';
24
24
  export * from './transform/transformTap.js';
25
25
  export * from './transform/transformThrottle.js';
26
+ export * from './transform/transformThrottleByRSS.js';
26
27
  export * from './transform/transformWarmup.js';
27
28
  export * from './transform/worker/baseWorkerClass.js';
28
29
  export * from './transform/worker/transformMultiThreaded.js';
@@ -9,6 +9,7 @@ import type { TransformMapSimpleOptions } from './transform/transformMapSimple.j
9
9
  import type { TransformMapSyncOptions } from './transform/transformMapSync.js';
10
10
  import type { TransformOffsetOptions } from './transform/transformOffset.js';
11
11
  import type { TransformThrottleOptions } from './transform/transformThrottle.js';
12
+ import type { TransformThrottleByRSSOptions } from './transform/transformThrottleByRSS.js';
12
13
  import type { TransformWarmupOptions } from './transform/transformWarmup.js';
13
14
  export declare class Pipeline<T = unknown> {
14
15
  private readonly source;
@@ -70,6 +71,7 @@ export declare class Pipeline<T = unknown> {
70
71
  tap(fn: AsyncIndexedMapper<T, any>, opt?: TransformOptions): this;
71
72
  tapSync(fn: IndexedMapper<T, any>, opt?: TransformOptions): this;
72
73
  throttle(opt: TransformThrottleOptions): this;
74
+ throttleByRSS(opt: TransformThrottleByRSSOptions): this;
73
75
  /**
74
76
  * @experimental to be removed after transformMap2 is stable
75
77
  */
@@ -24,6 +24,7 @@ import { transformOffset } from './transform/transformOffset.js';
24
24
  import { transformSplitOnNewline } from './transform/transformSplit.js';
25
25
  import { transformTap, transformTapSync } from './transform/transformTap.js';
26
26
  import { transformThrottle } from './transform/transformThrottle.js';
27
+ import { transformThrottleByRSS } from './transform/transformThrottleByRSS.js';
27
28
  import { transformWarmup } from './transform/transformWarmup.js';
28
29
  import { writablePushToArray } from './writable/writablePushToArray.js';
29
30
  import { writableVoid } from './writable/writableVoid.js';
@@ -175,6 +176,10 @@ export class Pipeline {
175
176
  this.transforms.push(transformThrottle(opt));
176
177
  return this;
177
178
  }
179
+ throttleByRSS(opt) {
180
+ this.transforms.push(transformThrottleByRSS(opt));
181
+ return this;
182
+ }
178
183
  /**
179
184
  * @experimental to be removed after transformMap2 is stable
180
185
  */
@@ -11,6 +11,7 @@ export function transformChunk(chunkSize, opt) {
11
11
  let buf = [];
12
12
  return new Transform({
13
13
  objectMode: true,
14
+ highWaterMark: 1,
14
15
  ...opt,
15
16
  transform(chunk, _, cb) {
16
17
  buf.push(chunk);
@@ -16,6 +16,7 @@ export function transformFilterSync(predicate, opt = {}) {
16
16
  let index = 0;
17
17
  return new Transform({
18
18
  objectMode: true,
19
+ highWaterMark: 1,
19
20
  ...opt,
20
21
  transform(chunk, _, cb) {
21
22
  try {
@@ -2,6 +2,7 @@ import { Transform } from 'node:stream';
2
2
  export function transformFlatten() {
3
3
  return new Transform({
4
4
  objectMode: true,
5
+ highWaterMark: 1,
5
6
  transform(chunk, _, cb) {
6
7
  for (const item of chunk) {
7
8
  this.push(item);
@@ -13,6 +14,7 @@ export function transformFlatten() {
13
14
  export function transformFlattenIfNeeded() {
14
15
  return new Transform({
15
16
  objectMode: true,
17
+ highWaterMark: 1,
16
18
  transform(chunk, _, cb) {
17
19
  if (Array.isArray(chunk)) {
18
20
  for (const item of chunk) {
@@ -12,7 +12,7 @@ import { createReadable } from '../readable/createReadable.js';
12
12
  * @experimental
13
13
  */
14
14
  export function transformFork(fn, opt = {}) {
15
- const { objectMode = true, highWaterMark } = opt;
15
+ const { objectMode = true, highWaterMark = 1 } = opt;
16
16
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
17
17
  let lock;
18
18
  const fork = createReadable([], {}, () => {
@@ -2,7 +2,7 @@ import { Transform } from 'node:stream';
2
2
  import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
3
3
  import { transformNoOp } from './transformNoOp.js';
4
4
  export function transformLimit(opt) {
5
- const { limit, signal, objectMode = true, highWaterMark } = opt;
5
+ const { limit, signal, objectMode = true, highWaterMark = 1 } = opt;
6
6
  if (!limit) {
7
7
  return transformNoOp();
8
8
  }
@@ -11,7 +11,7 @@ import { ErrorMode } from '@naturalcycles/js-lib/error/errorMode.js';
11
11
  */
12
12
  export function transformMapSimple(mapper, opt = {}) {
13
13
  let index = -1;
14
- const { errorMode = ErrorMode.THROW_IMMEDIATELY, logger = console, objectMode = true, highWaterMark, } = opt;
14
+ const { errorMode = ErrorMode.THROW_IMMEDIATELY, logger = console, objectMode = true, highWaterMark = 1, } = opt;
15
15
  return new Transform({
16
16
  objectMode,
17
17
  highWaterMark,
@@ -20,6 +20,7 @@ export function transformMapSync(mapper, opt = {}) {
20
20
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
21
21
  return new Transform({
22
22
  objectMode,
23
+ highWaterMark: 1,
23
24
  ...opt,
24
25
  transform(chunk, _, cb) {
25
26
  // Stop processing if isSettled
@@ -7,6 +7,7 @@ import { Transform } from 'node:stream';
7
7
  export function transformNoOp() {
8
8
  return new Transform({
9
9
  objectMode: true,
10
+ highWaterMark: 1,
10
11
  transform(chunk, _, cb) {
11
12
  cb(null, chunk);
12
13
  },
@@ -9,6 +9,7 @@ export function transformOffset(opt) {
9
9
  let i = 0; // so we start first chunk with 1
10
10
  return new Transform({
11
11
  objectMode: true,
12
+ highWaterMark: 1,
12
13
  ...opt,
13
14
  transform(chunk, _, cb) {
14
15
  if (++i <= offset) {
@@ -20,7 +20,7 @@ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
20
20
  * @experimental
21
21
  */
22
22
  export function transformThrottle(opt) {
23
- const { throughput, interval, objectMode = true, highWaterMark } = opt;
23
+ const { throughput, interval, objectMode = true, highWaterMark = 1 } = opt;
24
24
  let count = 0;
25
25
  let start;
26
26
  let lock;
@@ -0,0 +1,43 @@
1
+ import type { Integer, NumberOfMilliseconds } from '@naturalcycles/js-lib/types';
2
+ import type { TransformOptions, TransformTyped } from '../stream.model.js';
3
+ export interface TransformThrottleByRSSOptions extends TransformOptions {
4
+ /**
5
+ * Maximum RSS (Resident Set Size) in megabytes.
6
+ * When process RSS exceeds this value, the stream will pause
7
+ * until RSS drops below the threshold.
8
+ */
9
+ maxRSS: Integer;
10
+ /**
11
+ * How often to re-check RSS (in milliseconds) while paused.
12
+ *
13
+ * @default 5000
14
+ */
15
+ pollInterval?: NumberOfMilliseconds;
16
+ /**
17
+ * If this timeout is reached while RSS is above the limit -
18
+ * the transform will "give up", log the bold warning, and "open the gateways".
19
+ * Things will likely OOM after that, but at least it will not "hang forever".
20
+ *
21
+ * @default 30 minutes
22
+ */
23
+ pollTimeout?: NumberOfMilliseconds;
24
+ /**
25
+ * What to do if pollTimeout is reached.
26
+ * 'open-the-floodgates' will disable this throttle completely (YOLO).
27
+ * 'throw' will throw an error, which will destroy the stream/Pipeline.
28
+ *
29
+ * @default 'open-the-floodgates'
30
+ */
31
+ onPollTimeout?: 'open-the-floodgates' | 'throw';
32
+ }
33
+ /**
34
+ * Throttles the stream based on process memory (RSS) usage.
35
+ * When RSS exceeds `maxRSS` (in megabytes), the stream pauses
36
+ * and periodically re-checks until RSS drops below the threshold.
37
+ *
38
+ * Useful for pipelines that process large amounts of data and
39
+ * may cause memory pressure (e.g. database imports, file processing).
40
+ *
41
+ * @experimental
42
+ */
43
+ export declare function transformThrottleByRSS<T>(opt: TransformThrottleByRSSOptions): TransformTyped<T, T>;
@@ -0,0 +1,89 @@
1
+ import { Transform } from 'node:stream';
2
+ import { _mb } from '@naturalcycles/js-lib';
3
+ import { _ms, localTime } from '@naturalcycles/js-lib/datetime';
4
+ import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
5
+ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
6
+ /**
7
+ * Throttles the stream based on process memory (RSS) usage.
8
+ * When RSS exceeds `maxRSS` (in megabytes), the stream pauses
9
+ * and periodically re-checks until RSS drops below the threshold.
10
+ *
11
+ * Useful for pipelines that process large amounts of data and
12
+ * may cause memory pressure (e.g. database imports, file processing).
13
+ *
14
+ * @experimental
15
+ */
16
+ export function transformThrottleByRSS(opt) {
17
+ const { maxRSS, pollInterval = 5000, pollTimeout = 30 * 60_000, // 30 min
18
+ onPollTimeout = 'open-the-floodgates', objectMode = true, highWaterMark = 1, } = opt;
19
+ const maxRSSBytes = maxRSS * 1024 * 1024;
20
+ let lock;
21
+ let pollTimer;
22
+ let rssCheckTimer;
23
+ let lastRSS = 0;
24
+ let pausedSince = 0;
25
+ let disabled = false;
26
+ const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
27
+ return new Transform({
28
+ objectMode,
29
+ highWaterMark,
30
+ async transform(item, _, cb) {
31
+ if (lock) {
32
+ try {
33
+ await lock;
34
+ }
35
+ catch (err) {
36
+ cb(err);
37
+ return;
38
+ }
39
+ }
40
+ if (!disabled && lastRSS > maxRSSBytes && !lock) {
41
+ lock = pDefer();
42
+ pausedSince = Date.now();
43
+ logger.log(`${localTime.now().toPretty()} transformThrottleByRSS paused: RSS ${_mb(lastRSS)} > ${maxRSS} MB`);
44
+ pollTimer = setTimeout(() => pollRSS(), pollInterval);
45
+ }
46
+ cb(null, item);
47
+ },
48
+ construct(cb) {
49
+ // Start periodic RSS checking
50
+ checkRSS();
51
+ cb();
52
+ },
53
+ final(cb) {
54
+ clearTimeout(pollTimer);
55
+ clearTimeout(rssCheckTimer);
56
+ cb();
57
+ },
58
+ });
59
+ function checkRSS() {
60
+ lastRSS = process.memoryUsage.rss();
61
+ rssCheckTimer = setTimeout(() => checkRSS(), pollInterval);
62
+ }
63
+ function pollRSS() {
64
+ const rss = lastRSS;
65
+ if (rss <= maxRSSBytes) {
66
+ logger.log(`${localTime.now().toPretty()} transformThrottleByRSS resumed: RSS ${_mb(rss)} <= ${maxRSS} MB`);
67
+ lock.resolve();
68
+ lock = undefined;
69
+ }
70
+ else if (pollTimeout && Date.now() - pausedSince >= pollTimeout) {
71
+ clearTimeout(rssCheckTimer);
72
+ if (onPollTimeout === 'throw') {
73
+ lock.reject(new Error(`transformThrottleByRSS pollTimeout of ${_ms(pollTimeout)} reached, RSS ${_mb(rss)} still > ${maxRSS} MB`));
74
+ lock = undefined;
75
+ }
76
+ else {
77
+ // open-the-floodgates
78
+ logger.error(`${localTime.now().toPretty()} transformThrottleByRSS: pollTimeout of ${_ms(pollTimeout)} reached, RSS ${_mb(rss)} still > ${maxRSS} MB — DISABLING THROTTLE`);
79
+ disabled = true;
80
+ lock.resolve();
81
+ lock = undefined;
82
+ }
83
+ }
84
+ else {
85
+ logger.log(`${localTime.now().toPretty()} transformThrottleByRSS still paused: RSS ${_mb(rss)} > ${maxRSS} MB, rechecking in ${_ms(pollInterval)}`);
86
+ pollTimer = setTimeout(() => pollRSS(), pollInterval);
87
+ }
88
+ }
89
+ }
@@ -12,7 +12,7 @@ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js';
12
12
  * @experimental
13
13
  */
14
14
  export function transformWarmup(opt) {
15
- const { concurrency, warmupSeconds, objectMode = true, highWaterMark } = opt;
15
+ const { concurrency, warmupSeconds, objectMode = true, highWaterMark = 1 } = opt;
16
16
  const warmupMs = warmupSeconds * 1000;
17
17
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
18
18
  let startTime = 0;
@@ -1,11 +1,13 @@
1
1
  import type { ZlibOptions, ZstdOptions } from 'node:zlib';
2
2
  import type { Integer } from '@naturalcycles/js-lib/types';
3
3
  export declare function decompressZstdOrInflateToString(buf: Buffer): Promise<string>;
4
+ export declare function decompressZstdOrInflateToStringSync(buf: Buffer): string;
4
5
  /**
5
6
  * Detects if Buffer is zstd-compressed.
6
7
  * Otherwise attempts to Inflate.
7
8
  */
8
9
  export declare function decompressZstdOrInflate(buf: Buffer): Promise<Buffer<ArrayBuffer>>;
10
+ export declare function decompressZstdOrInflateSync(buf: Buffer): Buffer<ArrayBuffer>;
9
11
  /**
10
12
  * deflateBuffer uses `deflate`.
11
13
  * It's 9 bytes shorter than `gzip`.
@@ -9,6 +9,9 @@ const zstdDecompressAsync = promisify(zlib.zstdDecompress.bind(zlib));
9
9
  export async function decompressZstdOrInflateToString(buf) {
10
10
  return (await decompressZstdOrInflate(buf)).toString();
11
11
  }
12
+ export function decompressZstdOrInflateToStringSync(buf) {
13
+ return decompressZstdOrInflateSync(buf).toString();
14
+ }
12
15
  /**
13
16
  * Detects if Buffer is zstd-compressed.
14
17
  * Otherwise attempts to Inflate.
@@ -19,6 +22,12 @@ export async function decompressZstdOrInflate(buf) {
19
22
  }
20
23
  return await inflate(buf);
21
24
  }
25
+ export function decompressZstdOrInflateSync(buf) {
26
+ if (isZstdBuffer(buf)) {
27
+ return zlib.zstdDecompressSync(buf);
28
+ }
29
+ return zlib.inflateSync(buf);
30
+ }
22
31
  /**
23
32
  * deflateBuffer uses `deflate`.
24
33
  * It's 9 bytes shorter than `gzip`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.102.0",
4
+ "version": "15.104.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -23,6 +23,7 @@ export * from './transform/transformOffset.js'
23
23
  export * from './transform/transformSplit.js'
24
24
  export * from './transform/transformTap.js'
25
25
  export * from './transform/transformThrottle.js'
26
+ export * from './transform/transformThrottleByRSS.js'
26
27
  export * from './transform/transformWarmup.js'
27
28
  export * from './transform/worker/baseWorkerClass.js'
28
29
  export * from './transform/worker/transformMultiThreaded.js'
@@ -51,6 +51,8 @@ import { transformSplitOnNewline } from './transform/transformSplit.js'
51
51
  import { transformTap, transformTapSync } from './transform/transformTap.js'
52
52
  import { transformThrottle } from './transform/transformThrottle.js'
53
53
  import type { TransformThrottleOptions } from './transform/transformThrottle.js'
54
+ import { transformThrottleByRSS } from './transform/transformThrottleByRSS.js'
55
+ import type { TransformThrottleByRSSOptions } from './transform/transformThrottleByRSS.js'
54
56
  import { transformWarmup } from './transform/transformWarmup.js'
55
57
  import type { TransformWarmupOptions } from './transform/transformWarmup.js'
56
58
  import { writablePushToArray } from './writable/writablePushToArray.js'
@@ -249,6 +251,11 @@ export class Pipeline<T = unknown> {
249
251
  return this
250
252
  }
251
253
 
254
+ throttleByRSS(opt: TransformThrottleByRSSOptions): this {
255
+ this.transforms.push(transformThrottleByRSS(opt))
256
+ return this
257
+ }
258
+
252
259
  /**
253
260
  * @experimental to be removed after transformMap2 is stable
254
261
  */
@@ -18,6 +18,7 @@ export function transformChunk<IN = any>(
18
18
 
19
19
  return new Transform({
20
20
  objectMode: true,
21
+ highWaterMark: 1,
21
22
  ...opt,
22
23
  transform(chunk, _, cb) {
23
24
  buf.push(chunk)
@@ -28,6 +28,7 @@ export function transformFilterSync<IN = any>(
28
28
 
29
29
  return new Transform({
30
30
  objectMode: true,
31
+ highWaterMark: 1,
31
32
  ...opt,
32
33
  transform(chunk: IN, _, cb) {
33
34
  try {
@@ -4,6 +4,7 @@ import type { TransformTyped } from '../stream.model.js'
4
4
  export function transformFlatten<T>(): TransformTyped<T[], T> {
5
5
  return new Transform({
6
6
  objectMode: true,
7
+ highWaterMark: 1,
7
8
  transform(chunk: T[], _, cb) {
8
9
  for (const item of chunk) {
9
10
  this.push(item)
@@ -16,6 +17,7 @@ export function transformFlatten<T>(): TransformTyped<T[], T> {
16
17
  export function transformFlattenIfNeeded<T>(): TransformTyped<T[], T> {
17
18
  return new Transform({
18
19
  objectMode: true,
20
+ highWaterMark: 1,
19
21
  transform(chunk: T[], _, cb) {
20
22
  if (Array.isArray(chunk)) {
21
23
  for (const item of chunk) {
@@ -18,7 +18,7 @@ export function transformFork<T>(
18
18
  fn: (pipeline: Pipeline<T>) => Promise<void>,
19
19
  opt: TransformOptions = {},
20
20
  ): TransformTyped<T, T> {
21
- const { objectMode = true, highWaterMark } = opt
21
+ const { objectMode = true, highWaterMark = 1 } = opt
22
22
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
23
23
 
24
24
  let lock: DeferredPromise | undefined
@@ -18,7 +18,7 @@ export interface TransformLimitOptions extends TransformOptions {
18
18
  }
19
19
 
20
20
  export function transformLimit<IN>(opt: TransformLimitOptions): TransformTyped<IN, IN> {
21
- const { limit, signal, objectMode = true, highWaterMark } = opt
21
+ const { limit, signal, objectMode = true, highWaterMark = 1 } = opt
22
22
 
23
23
  if (!limit) {
24
24
  return transformNoOp()
@@ -30,7 +30,7 @@ export function transformMapSimple<IN = any, OUT = IN>(
30
30
  errorMode = ErrorMode.THROW_IMMEDIATELY,
31
31
  logger = console,
32
32
  objectMode = true,
33
- highWaterMark,
33
+ highWaterMark = 1,
34
34
  } = opt
35
35
 
36
36
  return new Transform({
@@ -88,6 +88,7 @@ export function transformMapSync<IN = any, OUT = IN>(
88
88
 
89
89
  return new Transform({
90
90
  objectMode,
91
+ highWaterMark: 1,
91
92
  ...opt,
92
93
  transform(chunk: IN, _, cb) {
93
94
  // Stop processing if isSettled
@@ -9,6 +9,7 @@ import type { TransformTyped } from '../stream.model.js'
9
9
  export function transformNoOp<T = any>(): TransformTyped<T, T> {
10
10
  return new Transform({
11
11
  objectMode: true,
12
+ highWaterMark: 1,
12
13
  transform(chunk: T, _, cb) {
13
14
  cb(null, chunk)
14
15
  },
@@ -22,6 +22,7 @@ export function transformOffset<IN>(opt: TransformOffsetOptions): TransformTyped
22
22
  let i = 0 // so we start first chunk with 1
23
23
  return new Transform({
24
24
  objectMode: true,
25
+ highWaterMark: 1,
25
26
  ...opt,
26
27
  transform(chunk: IN, _, cb) {
27
28
  if (++i <= offset) {
@@ -40,7 +40,7 @@ export interface TransformThrottleOptions extends TransformOptions {
40
40
  * @experimental
41
41
  */
42
42
  export function transformThrottle<T>(opt: TransformThrottleOptions): TransformTyped<T, T> {
43
- const { throughput, interval, objectMode = true, highWaterMark } = opt
43
+ const { throughput, interval, objectMode = true, highWaterMark = 1 } = opt
44
44
 
45
45
  let count = 0
46
46
  let start: UnixTimestampMillis
@@ -0,0 +1,150 @@
1
+ import { Transform } from 'node:stream'
2
+ import { _mb } from '@naturalcycles/js-lib'
3
+ import { _ms, localTime } from '@naturalcycles/js-lib/datetime'
4
+ import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
5
+ import type { DeferredPromise } from '@naturalcycles/js-lib/promise'
6
+ import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'
7
+ import type { Integer, NumberOfMilliseconds } from '@naturalcycles/js-lib/types'
8
+ import type { TransformOptions, TransformTyped } from '../stream.model.js'
9
+
10
+ export interface TransformThrottleByRSSOptions extends TransformOptions {
11
+ /**
12
+ * Maximum RSS (Resident Set Size) in megabytes.
13
+ * When process RSS exceeds this value, the stream will pause
14
+ * until RSS drops below the threshold.
15
+ */
16
+ maxRSS: Integer
17
+
18
+ /**
19
+ * How often to re-check RSS (in milliseconds) while paused.
20
+ *
21
+ * @default 5000
22
+ */
23
+ pollInterval?: NumberOfMilliseconds
24
+
25
+ /**
26
+ * If this timeout is reached while RSS is above the limit -
27
+ * the transform will "give up", log the bold warning, and "open the gateways".
28
+ * Things will likely OOM after that, but at least it will not "hang forever".
29
+ *
30
+ * @default 30 minutes
31
+ */
32
+ pollTimeout?: NumberOfMilliseconds
33
+
34
+ /**
35
+ * What to do if pollTimeout is reached.
36
+ * 'open-the-floodgates' will disable this throttle completely (YOLO).
37
+ * 'throw' will throw an error, which will destroy the stream/Pipeline.
38
+ *
39
+ * @default 'open-the-floodgates'
40
+ */
41
+ onPollTimeout?: 'open-the-floodgates' | 'throw'
42
+ }
43
+
44
+ /**
45
+ * Throttles the stream based on process memory (RSS) usage.
46
+ * When RSS exceeds `maxRSS` (in megabytes), the stream pauses
47
+ * and periodically re-checks until RSS drops below the threshold.
48
+ *
49
+ * Useful for pipelines that process large amounts of data and
50
+ * may cause memory pressure (e.g. database imports, file processing).
51
+ *
52
+ * @experimental
53
+ */
54
+ export function transformThrottleByRSS<T>(
55
+ opt: TransformThrottleByRSSOptions,
56
+ ): TransformTyped<T, T> {
57
+ const {
58
+ maxRSS,
59
+ pollInterval = 5000,
60
+ pollTimeout = 30 * 60_000, // 30 min
61
+ onPollTimeout = 'open-the-floodgates',
62
+ objectMode = true,
63
+ highWaterMark = 1,
64
+ } = opt
65
+
66
+ const maxRSSBytes = maxRSS * 1024 * 1024
67
+ let lock: DeferredPromise | undefined
68
+ let pollTimer: NodeJS.Timeout | undefined
69
+ let rssCheckTimer: NodeJS.Timeout | undefined
70
+ let lastRSS = 0
71
+ let pausedSince = 0
72
+ let disabled = false
73
+ const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
74
+
75
+ return new Transform({
76
+ objectMode,
77
+ highWaterMark,
78
+ async transform(item: T, _, cb) {
79
+ if (lock) {
80
+ try {
81
+ await lock
82
+ } catch (err) {
83
+ cb(err as Error)
84
+ return
85
+ }
86
+ }
87
+
88
+ if (!disabled && lastRSS > maxRSSBytes && !lock) {
89
+ lock = pDefer()
90
+ pausedSince = Date.now()
91
+ logger.log(
92
+ `${localTime.now().toPretty()} transformThrottleByRSS paused: RSS ${_mb(lastRSS)} > ${maxRSS} MB`,
93
+ )
94
+ pollTimer = setTimeout(() => pollRSS(), pollInterval)
95
+ }
96
+
97
+ cb(null, item)
98
+ },
99
+ construct(cb) {
100
+ // Start periodic RSS checking
101
+ checkRSS()
102
+ cb()
103
+ },
104
+ final(cb) {
105
+ clearTimeout(pollTimer)
106
+ clearTimeout(rssCheckTimer)
107
+ cb()
108
+ },
109
+ })
110
+
111
+ function checkRSS(): void {
112
+ lastRSS = process.memoryUsage.rss()
113
+ rssCheckTimer = setTimeout(() => checkRSS(), pollInterval)
114
+ }
115
+
116
+ function pollRSS(): void {
117
+ const rss = lastRSS
118
+
119
+ if (rss <= maxRSSBytes) {
120
+ logger.log(
121
+ `${localTime.now().toPretty()} transformThrottleByRSS resumed: RSS ${_mb(rss)} <= ${maxRSS} MB`,
122
+ )
123
+ lock!.resolve()
124
+ lock = undefined
125
+ } else if (pollTimeout && Date.now() - pausedSince >= pollTimeout) {
126
+ clearTimeout(rssCheckTimer)
127
+ if (onPollTimeout === 'throw') {
128
+ lock!.reject(
129
+ new Error(
130
+ `transformThrottleByRSS pollTimeout of ${_ms(pollTimeout)} reached, RSS ${_mb(rss)} still > ${maxRSS} MB`,
131
+ ),
132
+ )
133
+ lock = undefined
134
+ } else {
135
+ // open-the-floodgates
136
+ logger.error(
137
+ `${localTime.now().toPretty()} transformThrottleByRSS: pollTimeout of ${_ms(pollTimeout)} reached, RSS ${_mb(rss)} still > ${maxRSS} MB — DISABLING THROTTLE`,
138
+ )
139
+ disabled = true
140
+ lock!.resolve()
141
+ lock = undefined
142
+ }
143
+ } else {
144
+ logger.log(
145
+ `${localTime.now().toPretty()} transformThrottleByRSS still paused: RSS ${_mb(rss)} > ${maxRSS} MB, rechecking in ${_ms(pollInterval)}`,
146
+ )
147
+ pollTimer = setTimeout(() => pollRSS(), pollInterval)
148
+ }
149
+ }
150
+ }
@@ -29,7 +29,7 @@ export interface TransformWarmupOptions extends TransformOptions {
29
29
  * @experimental
30
30
  */
31
31
  export function transformWarmup<T>(opt: TransformWarmupOptions): TransformTyped<T, T> {
32
- const { concurrency, warmupSeconds, objectMode = true, highWaterMark } = opt
32
+ const { concurrency, warmupSeconds, objectMode = true, highWaterMark = 1 } = opt
33
33
  const warmupMs = warmupSeconds * 1000
34
34
  const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel)
35
35
 
@@ -14,6 +14,10 @@ export async function decompressZstdOrInflateToString(buf: Buffer): Promise<stri
14
14
  return (await decompressZstdOrInflate(buf)).toString()
15
15
  }
16
16
 
17
+ export function decompressZstdOrInflateToStringSync(buf: Buffer): string {
18
+ return decompressZstdOrInflateSync(buf).toString()
19
+ }
20
+
17
21
  /**
18
22
  * Detects if Buffer is zstd-compressed.
19
23
  * Otherwise attempts to Inflate.
@@ -25,6 +29,13 @@ export async function decompressZstdOrInflate(buf: Buffer): Promise<Buffer<Array
25
29
  return await inflate(buf)
26
30
  }
27
31
 
32
+ export function decompressZstdOrInflateSync(buf: Buffer): Buffer<ArrayBuffer> {
33
+ if (isZstdBuffer(buf)) {
34
+ return zlib.zstdDecompressSync(buf)
35
+ }
36
+ return zlib.inflateSync(buf)
37
+ }
38
+
28
39
  /**
29
40
  * deflateBuffer uses `deflate`.
30
41
  * It's 9 bytes shorter than `gzip`.