@naturalcycles/js-lib 15.57.0 → 15.58.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,5 +1,5 @@
1
1
  export * from './assert.js';
2
- export * from './error.model.js';
2
+ export type * from './error.model.js';
3
3
  export * from './error.util.js';
4
4
  export * from './errorMode.js';
5
5
  export * from './try.js';
@@ -1,5 +1,4 @@
1
1
  export * from './assert.js';
2
- export * from './error.model.js';
3
2
  export * from './error.util.js';
4
3
  export * from './errorMode.js';
5
4
  export * from './try.js';
@@ -2,6 +2,7 @@ export * from './abortable.js';
2
2
  export * from './pDefer.js';
3
3
  export * from './pDelay.js';
4
4
  export * from './pFilter.js';
5
+ export * from './pGradualQueue.js';
5
6
  export * from './pHang.js';
6
7
  export * from './pMap.js';
7
8
  export * from './pProps.js';
@@ -2,6 +2,7 @@ export * from './abortable.js';
2
2
  export * from './pDefer.js';
3
3
  export * from './pDelay.js';
4
4
  export * from './pFilter.js';
5
+ export * from './pGradualQueue.js';
5
6
  export * from './pHang.js';
6
7
  export * from './pMap.js';
7
8
  export * from './pProps.js';
@@ -0,0 +1,61 @@
1
+ import { ErrorMode } from '../error/errorMode.js';
2
+ import { type CommonLogger, type CommonLogLevel } from '../log/commonLogger.js';
3
+ import type { AsyncFunction, PositiveInteger } from '../types.js';
4
+ export interface PGradualQueueCfg {
5
+ concurrency: PositiveInteger;
6
+ /**
7
+ * Time in seconds to gradually increase concurrency from 1 to cfg.concurrency.
8
+ * After this period, the queue operates at full concurrency.
9
+ *
10
+ * Set to 0 to disable warmup (behaves like regular PQueue).
11
+ */
12
+ warmupSeconds: number;
13
+ /**
14
+ * Default: THROW_IMMEDIATELY
15
+ *
16
+ * THROW_AGGREGATED is not supported.
17
+ *
18
+ * SUPPRESS_ERRORS will still log errors via logger. It will resolve the `.push` promise with void.
19
+ */
20
+ errorMode?: ErrorMode;
21
+ /**
22
+ * Default to `console`
23
+ */
24
+ logger?: CommonLogger;
25
+ /**
26
+ * Default is 'log'.
27
+ */
28
+ logLevel?: CommonLogLevel;
29
+ }
30
+ /**
31
+ * A queue similar to PQueue that gradually increases concurrency from 1 to the configured
32
+ * maximum over a warmup period. Useful for scenarios where you want to avoid overwhelming
33
+ * a system at startup (e.g., database connections, API rate limits).
34
+ *
35
+ * API is @experimental
36
+ */
37
+ export declare class PGradualQueue {
38
+ constructor(cfg: PGradualQueueCfg);
39
+ private readonly cfg;
40
+ private readonly logger;
41
+ private readonly warmupMs;
42
+ private startTime;
43
+ private warmupComplete;
44
+ inFlight: number;
45
+ private queue;
46
+ /**
47
+ * Get current allowed concurrency based on warmup progress.
48
+ * Returns cfg.concurrency if warmup is complete (fast-path).
49
+ */
50
+ private getCurrentConcurrency;
51
+ /**
52
+ * Push PromiseReturningFunction to the Queue.
53
+ * Returns a Promise that resolves (or rejects) with the return value from the Promise.
54
+ */
55
+ push<R>(fn_: AsyncFunction<R>): Promise<R>;
56
+ get queueSize(): number;
57
+ /**
58
+ * Current concurrency limit based on warmup progress.
59
+ */
60
+ get currentConcurrency(): number;
61
+ }
@@ -0,0 +1,115 @@
1
+ import { ErrorMode } from '../error/errorMode.js';
2
+ import { createCommonLoggerAtLevel, } from '../log/commonLogger.js';
3
+ import { pDefer } from './pDefer.js';
4
+ /**
5
+ * A queue similar to PQueue that gradually increases concurrency from 1 to the configured
6
+ * maximum over a warmup period. Useful for scenarios where you want to avoid overwhelming
7
+ * a system at startup (e.g., database connections, API rate limits).
8
+ *
9
+ * API is @experimental
10
+ */
11
+ export class PGradualQueue {
12
+ constructor(cfg) {
13
+ this.cfg = {
14
+ errorMode: ErrorMode.THROW_IMMEDIATELY,
15
+ ...cfg,
16
+ };
17
+ this.logger = createCommonLoggerAtLevel(cfg.logger, cfg.logLevel);
18
+ this.warmupMs = cfg.warmupSeconds * 1000;
19
+ // Fast-path: if warmupSeconds is 0 or concurrency is 1, skip warmup entirely
20
+ this.warmupComplete = cfg.warmupSeconds <= 0 || cfg.concurrency <= 1;
21
+ }
22
+ cfg;
23
+ logger;
24
+ warmupMs;
25
+ startTime = 0;
26
+ warmupComplete;
27
+ inFlight = 0;
28
+ queue = [];
29
+ /**
30
+ * Get current allowed concurrency based on warmup progress.
31
+ * Returns cfg.concurrency if warmup is complete (fast-path).
32
+ */
33
+ getCurrentConcurrency() {
34
+ // Fast-path: warmup complete
35
+ if (this.warmupComplete)
36
+ return this.cfg.concurrency;
37
+ const elapsed = Date.now() - this.startTime;
38
+ if (elapsed >= this.warmupMs) {
39
+ this.warmupComplete = true;
40
+ this.logger.debug('warmup complete');
41
+ return this.cfg.concurrency;
42
+ }
43
+ // Linear interpolation from 1 to concurrency
44
+ const progress = elapsed / this.warmupMs;
45
+ return Math.max(1, Math.floor(1 + (this.cfg.concurrency - 1) * progress));
46
+ }
47
+ /**
48
+ * Push PromiseReturningFunction to the Queue.
49
+ * Returns a Promise that resolves (or rejects) with the return value from the Promise.
50
+ */
51
+ async push(fn_) {
52
+ // Initialize start time on first push
53
+ if (this.startTime === 0) {
54
+ this.startTime = Date.now();
55
+ }
56
+ const { logger } = this;
57
+ const fn = fn_;
58
+ fn.defer ||= pDefer();
59
+ const concurrency = this.getCurrentConcurrency();
60
+ if (this.inFlight < concurrency) {
61
+ this.inFlight++;
62
+ logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
63
+ runSafe(fn)
64
+ .then(result => {
65
+ fn.defer.resolve(result);
66
+ })
67
+ .catch((err) => {
68
+ if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
69
+ logger.error(err);
70
+ fn.defer.resolve(); // resolve with `void`
71
+ }
72
+ else {
73
+ fn.defer.reject(err);
74
+ }
75
+ })
76
+ .finally(() => {
77
+ this.inFlight--;
78
+ const currentConcurrency = this.getCurrentConcurrency();
79
+ logger.debug(`inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`);
80
+ // Start queued jobs up to the current concurrency limit
81
+ // Use while loop since concurrency may have increased during warmup
82
+ while (this.queue.length && this.inFlight < this.getCurrentConcurrency()) {
83
+ const nextFn = this.queue.shift();
84
+ void this.push(nextFn);
85
+ }
86
+ });
87
+ }
88
+ else {
89
+ this.queue.push(fn);
90
+ logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
91
+ }
92
+ return await fn.defer;
93
+ }
94
+ get queueSize() {
95
+ return this.queue.length;
96
+ }
97
+ /**
98
+ * Current concurrency limit based on warmup progress.
99
+ */
100
+ get currentConcurrency() {
101
+ return this.getCurrentConcurrency();
102
+ }
103
+ }
104
+ // Here we intentionally want it not async, as we don't want it to throw
105
+ // oxlint-disable-next-line typescript/promise-function-async
106
+ function runSafe(fn) {
107
+ try {
108
+ // Here we are intentionally not awaiting
109
+ return fn();
110
+ }
111
+ catch (err) {
112
+ // Handle synchronous throws
113
+ return Promise.reject(err);
114
+ }
115
+ }
@@ -1,7 +1,8 @@
1
1
  import { ErrorMode } from '../error/errorMode.js';
2
- import type { CommonLogger } from '../log/commonLogger.js';
2
+ import { type CommonLogger, type CommonLogLevel } from '../log/commonLogger.js';
3
+ import type { AsyncFunction, PositiveInteger } from '../types.js';
3
4
  export interface PQueueCfg {
4
- concurrency: number;
5
+ concurrency: PositiveInteger;
5
6
  /**
6
7
  * Default: THROW_IMMEDIATELY
7
8
  *
@@ -10,17 +11,14 @@ export interface PQueueCfg {
10
11
  * SUPPRESS_ERRORS will still log errors via logger. It will resolve the `.push` promise with void.
11
12
  */
12
13
  errorMode?: ErrorMode;
13
- /**
14
- * @default true
15
- */
16
14
  /**
17
15
  * Default to `console`
18
16
  */
19
17
  logger?: CommonLogger;
20
18
  /**
21
- * If true - will LOG EVERYTHING:)
19
+ * Default is 'log'.
22
20
  */
23
- debug?: boolean;
21
+ logLevel?: CommonLogLevel;
24
22
  /**
25
23
  * By default .push method resolves when the Promise is done (finished).
26
24
  *
@@ -31,7 +29,6 @@ export interface PQueueCfg {
31
29
  */
32
30
  resolveOn?: 'finish' | 'start';
33
31
  }
34
- export type PromiseReturningFunction<R> = () => Promise<R>;
35
32
  /**
36
33
  * Inspired by: https://github.com/sindresorhus/p-queue
37
34
  *
@@ -43,10 +40,16 @@ export type PromiseReturningFunction<R> = () => Promise<R>;
43
40
  export declare class PQueue {
44
41
  constructor(cfg: PQueueCfg);
45
42
  private readonly cfg;
46
- private debug;
43
+ private readonly resolveOnStart;
44
+ private readonly logger;
47
45
  inFlight: number;
48
46
  private queue;
49
47
  private onIdleListeners;
48
+ /**
49
+ * Push PromiseReturningFunction to the Queue.
50
+ * Returns a Promise that resolves (or rejects) with the return value from the Promise.
51
+ */
52
+ push<R>(fn_: AsyncFunction<R>): Promise<R>;
50
53
  get queueSize(): number;
51
54
  /**
52
55
  * Returns a Promise that resolves when the queue is Idle (next time, since the call).
@@ -54,9 +57,4 @@ export declare class PQueue {
54
57
  * Idle means 0 queue and 0 inFlight.
55
58
  */
56
59
  onIdle(): Promise<void>;
57
- /**
58
- * Push PromiseReturningFunction to the Queue.
59
- * Returns a Promise that resolves (or rejects) with the return value from the Promise.
60
- */
61
- push<R>(fn_: PromiseReturningFunction<R>): Promise<R>;
62
60
  }
@@ -1,4 +1,5 @@
1
1
  import { ErrorMode } from '../error/errorMode.js';
2
+ import { createCommonLoggerAtLevel, } from '../log/commonLogger.js';
2
3
  import { pDefer } from './pDefer.js';
3
4
  /**
4
5
  * Inspired by: https://github.com/sindresorhus/p-queue
@@ -11,74 +12,56 @@ import { pDefer } from './pDefer.js';
11
12
  export class PQueue {
12
13
  constructor(cfg) {
13
14
  this.cfg = {
14
- // concurrency: Number.MAX_SAFE_INTEGER,
15
15
  errorMode: ErrorMode.THROW_IMMEDIATELY,
16
- logger: console,
17
- debug: false,
18
- resolveOn: 'finish',
19
16
  ...cfg,
20
17
  };
21
- if (!cfg.debug) {
22
- this.debug = () => { };
23
- }
18
+ this.logger = createCommonLoggerAtLevel(cfg.logger, cfg.logLevel);
19
+ this.resolveOnStart = this.cfg.resolveOn === 'start';
24
20
  }
25
21
  cfg;
26
- debug(...args) {
27
- this.cfg.logger.log(...args);
28
- }
22
+ resolveOnStart;
23
+ logger;
29
24
  inFlight = 0;
30
25
  queue = [];
31
26
  onIdleListeners = [];
32
- get queueSize() {
33
- return this.queue.length;
34
- }
35
- /**
36
- * Returns a Promise that resolves when the queue is Idle (next time, since the call).
37
- * Resolves immediately in case the queue is Idle.
38
- * Idle means 0 queue and 0 inFlight.
39
- */
40
- async onIdle() {
41
- if (this.queue.length === 0 && this.inFlight === 0)
42
- return;
43
- const listener = pDefer();
44
- this.onIdleListeners.push(listener);
45
- return await listener;
46
- }
47
27
  /**
48
28
  * Push PromiseReturningFunction to the Queue.
49
29
  * Returns a Promise that resolves (or rejects) with the return value from the Promise.
50
30
  */
51
31
  async push(fn_) {
52
32
  const { concurrency } = this.cfg;
53
- const resolveOnStart = this.cfg.resolveOn === 'start';
33
+ const { resolveOnStart, logger } = this;
54
34
  const fn = fn_;
55
35
  fn.defer ||= pDefer();
56
36
  if (this.inFlight < concurrency) {
57
37
  // There is room for more jobs. Can start immediately
58
38
  this.inFlight++;
59
- this.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
39
+ logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
60
40
  if (resolveOnStart)
61
41
  fn.defer.resolve();
62
- fn()
42
+ runSafe(fn)
63
43
  .then(result => {
64
44
  if (!resolveOnStart)
65
45
  fn.defer.resolve(result);
66
46
  })
67
47
  .catch((err) => {
68
- this.cfg.logger.error(err);
69
- if (resolveOnStart)
48
+ if (resolveOnStart) {
49
+ logger.error(err);
70
50
  return;
51
+ }
71
52
  if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
53
+ logger.error(err);
72
54
  fn.defer.resolve(); // resolve with `void`
73
55
  }
74
56
  else {
75
57
  // Should be handled on the outside, otherwise it'll cause UnhandledRejection
58
+ // Not logging, because it's re-thrown upstream
76
59
  fn.defer.reject(err);
77
60
  }
78
61
  })
79
62
  .finally(() => {
80
63
  this.inFlight--;
81
- this.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
64
+ logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
82
65
  // check if there's room to start next job
83
66
  if (this.queue.length && this.inFlight <= concurrency) {
84
67
  const nextFn = this.queue.shift();
@@ -86,7 +69,7 @@ export class PQueue {
86
69
  }
87
70
  else {
88
71
  if (this.inFlight === 0) {
89
- this.debug('onIdle');
72
+ logger.debug('onIdle');
90
73
  this.onIdleListeners.forEach(defer => defer.resolve());
91
74
  this.onIdleListeners.length = 0; // empty the array
92
75
  }
@@ -95,8 +78,35 @@ export class PQueue {
95
78
  }
96
79
  else {
97
80
  this.queue.push(fn);
98
- this.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
81
+ logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
99
82
  }
100
83
  return await fn.defer;
101
84
  }
85
+ get queueSize() {
86
+ return this.queue.length;
87
+ }
88
+ /**
89
+ * Returns a Promise that resolves when the queue is Idle (next time, since the call).
90
+ * Resolves immediately in case the queue is Idle.
91
+ * Idle means 0 queue and 0 inFlight.
92
+ */
93
+ async onIdle() {
94
+ if (this.queue.length === 0 && this.inFlight === 0)
95
+ return;
96
+ const listener = pDefer();
97
+ this.onIdleListeners.push(listener);
98
+ return await listener;
99
+ }
100
+ }
101
+ // Here we intentionally want it not async, as we don't want it to throw
102
+ // oxlint-disable-next-line typescript/promise-function-async
103
+ function runSafe(fn) {
104
+ try {
105
+ // Here we are intentionally not awaiting
106
+ return fn();
107
+ }
108
+ catch (err) {
109
+ // Handle synchronous throws - ensure inFlight is decremented
110
+ return Promise.reject(err);
111
+ }
102
112
  }
package/dist/types.d.ts CHANGED
@@ -417,4 +417,4 @@ type ReadonlyObjectDeep<ObjectType extends object> = {
417
417
  ```
418
418
  */
419
419
  export type RequiredProp<T, K extends keyof T> = Required<Pick<T, K>> & T;
420
- export * from './typeFest.js';
420
+ export type * from './typeFest.js';
package/dist/types.js CHANGED
@@ -58,4 +58,3 @@ export function _typeCast(_v) { }
58
58
  * Type-safe Object.assign that checks that part is indeed a Partial<T>
59
59
  */
60
60
  export const _objectAssign = Object.assign;
61
- export * from './typeFest.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.57.0",
4
+ "version": "15.58.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2",
7
7
  "undici": "^7",
@@ -13,7 +13,7 @@
13
13
  "@types/semver": "^7",
14
14
  "crypto-js": "^4",
15
15
  "dayjs": "^1",
16
- "@naturalcycles/dev-lib": "20.15.0"
16
+ "@naturalcycles/dev-lib": "18.4.2"
17
17
  },
18
18
  "exports": {
19
19
  ".": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  export * from './assert.js'
2
- export * from './error.model.js'
2
+ export type * from './error.model.js'
3
3
  export * from './error.util.js'
4
4
  export * from './errorMode.js'
5
5
  export * from './try.js'
@@ -2,6 +2,7 @@ export * from './abortable.js'
2
2
  export * from './pDefer.js'
3
3
  export * from './pDelay.js'
4
4
  export * from './pFilter.js'
5
+ export * from './pGradualQueue.js'
5
6
  export * from './pHang.js'
6
7
  export * from './pMap.js'
7
8
  export * from './pProps.js'
@@ -0,0 +1,171 @@
1
+ import { ErrorMode } from '../error/errorMode.js'
2
+ import {
3
+ type CommonLogger,
4
+ type CommonLogLevel,
5
+ createCommonLoggerAtLevel,
6
+ } from '../log/commonLogger.js'
7
+ import type { AsyncFunction, PositiveInteger } from '../types.js'
8
+ import type { DeferredPromise } from './pDefer.js'
9
+ import { pDefer } from './pDefer.js'
10
+
11
+ export interface PGradualQueueCfg {
12
+ concurrency: PositiveInteger
13
+
14
+ /**
15
+ * Time in seconds to gradually increase concurrency from 1 to cfg.concurrency.
16
+ * After this period, the queue operates at full concurrency.
17
+ *
18
+ * Set to 0 to disable warmup (behaves like regular PQueue).
19
+ */
20
+ warmupSeconds: number
21
+
22
+ /**
23
+ * Default: THROW_IMMEDIATELY
24
+ *
25
+ * THROW_AGGREGATED is not supported.
26
+ *
27
+ * SUPPRESS_ERRORS will still log errors via logger. It will resolve the `.push` promise with void.
28
+ */
29
+ errorMode?: ErrorMode
30
+
31
+ /**
32
+ * Default to `console`
33
+ */
34
+ logger?: CommonLogger
35
+
36
+ /**
37
+ * Default is 'log'.
38
+ */
39
+ logLevel?: CommonLogLevel
40
+ }
41
+
42
+ /**
43
+ * A queue similar to PQueue that gradually increases concurrency from 1 to the configured
44
+ * maximum over a warmup period. Useful for scenarios where you want to avoid overwhelming
45
+ * a system at startup (e.g., database connections, API rate limits).
46
+ *
47
+ * API is @experimental
48
+ */
49
+ export class PGradualQueue {
50
+ constructor(cfg: PGradualQueueCfg) {
51
+ this.cfg = {
52
+ errorMode: ErrorMode.THROW_IMMEDIATELY,
53
+ ...cfg,
54
+ }
55
+ this.logger = createCommonLoggerAtLevel(cfg.logger, cfg.logLevel)
56
+ this.warmupMs = cfg.warmupSeconds * 1000
57
+ // Fast-path: if warmupSeconds is 0 or concurrency is 1, skip warmup entirely
58
+ this.warmupComplete = cfg.warmupSeconds <= 0 || cfg.concurrency <= 1
59
+ }
60
+
61
+ private readonly cfg: PGradualQueueCfg
62
+ private readonly logger: CommonLogger
63
+ private readonly warmupMs: number
64
+
65
+ private startTime = 0
66
+ private warmupComplete: boolean
67
+
68
+ inFlight = 0
69
+ private queue: AsyncFunction[] = []
70
+
71
+ /**
72
+ * Get current allowed concurrency based on warmup progress.
73
+ * Returns cfg.concurrency if warmup is complete (fast-path).
74
+ */
75
+ private getCurrentConcurrency(): number {
76
+ // Fast-path: warmup complete
77
+ if (this.warmupComplete) return this.cfg.concurrency
78
+
79
+ const elapsed = Date.now() - this.startTime
80
+ if (elapsed >= this.warmupMs) {
81
+ this.warmupComplete = true
82
+ this.logger.debug('warmup complete')
83
+ return this.cfg.concurrency
84
+ }
85
+
86
+ // Linear interpolation from 1 to concurrency
87
+ const progress = elapsed / this.warmupMs
88
+ return Math.max(1, Math.floor(1 + (this.cfg.concurrency - 1) * progress))
89
+ }
90
+
91
+ /**
92
+ * Push PromiseReturningFunction to the Queue.
93
+ * Returns a Promise that resolves (or rejects) with the return value from the Promise.
94
+ */
95
+ async push<R>(fn_: AsyncFunction<R>): Promise<R> {
96
+ // Initialize start time on first push
97
+ if (this.startTime === 0) {
98
+ this.startTime = Date.now()
99
+ }
100
+
101
+ const { logger } = this
102
+ const fn = fn_ as AsyncFunctionWithDefer<R>
103
+ fn.defer ||= pDefer<R>()
104
+
105
+ const concurrency = this.getCurrentConcurrency()
106
+
107
+ if (this.inFlight < concurrency) {
108
+ this.inFlight++
109
+ logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
110
+
111
+ runSafe(fn)
112
+ .then(result => {
113
+ fn.defer.resolve(result)
114
+ })
115
+ .catch((err: Error) => {
116
+ if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
117
+ logger.error(err)
118
+ fn.defer.resolve() // resolve with `void`
119
+ } else {
120
+ fn.defer.reject(err)
121
+ }
122
+ })
123
+ .finally(() => {
124
+ this.inFlight--
125
+ const currentConcurrency = this.getCurrentConcurrency()
126
+ logger.debug(
127
+ `inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`,
128
+ )
129
+
130
+ // Start queued jobs up to the current concurrency limit
131
+ // Use while loop since concurrency may have increased during warmup
132
+ while (this.queue.length && this.inFlight < this.getCurrentConcurrency()) {
133
+ const nextFn = this.queue.shift()!
134
+ void this.push(nextFn)
135
+ }
136
+ })
137
+ } else {
138
+ this.queue.push(fn)
139
+ logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`)
140
+ }
141
+
142
+ return await fn.defer
143
+ }
144
+
145
+ get queueSize(): number {
146
+ return this.queue.length
147
+ }
148
+
149
+ /**
150
+ * Current concurrency limit based on warmup progress.
151
+ */
152
+ get currentConcurrency(): number {
153
+ return this.getCurrentConcurrency()
154
+ }
155
+ }
156
+
157
+ // Here we intentionally want it not async, as we don't want it to throw
158
+ // oxlint-disable-next-line typescript/promise-function-async
159
+ function runSafe<R>(fn: AsyncFunction<R>): Promise<R> {
160
+ try {
161
+ // Here we are intentionally not awaiting
162
+ return fn()
163
+ } catch (err) {
164
+ // Handle synchronous throws
165
+ return Promise.reject(err as Error)
166
+ }
167
+ }
168
+
169
+ interface AsyncFunctionWithDefer<R = unknown> extends AsyncFunction<R> {
170
+ defer: DeferredPromise<R>
171
+ }
@@ -1,10 +1,15 @@
1
1
  import { ErrorMode } from '../error/errorMode.js'
2
- import type { CommonLogger } from '../log/commonLogger.js'
2
+ import {
3
+ type CommonLogger,
4
+ type CommonLogLevel,
5
+ createCommonLoggerAtLevel,
6
+ } from '../log/commonLogger.js'
7
+ import type { AsyncFunction, PositiveInteger } from '../types.js'
3
8
  import type { DeferredPromise } from './pDefer.js'
4
9
  import { pDefer } from './pDefer.js'
5
10
 
6
11
  export interface PQueueCfg {
7
- concurrency: number
12
+ concurrency: PositiveInteger
8
13
 
9
14
  /**
10
15
  * Default: THROW_IMMEDIATELY
@@ -15,25 +20,15 @@ export interface PQueueCfg {
15
20
  */
16
21
  errorMode?: ErrorMode
17
22
 
18
- /**
19
- * @default true
20
- */
21
- // autoStart?: boolean
22
-
23
23
  /**
24
24
  * Default to `console`
25
25
  */
26
26
  logger?: CommonLogger
27
27
 
28
28
  /**
29
- * If true - will LOG EVERYTHING:)
29
+ * Default is 'log'.
30
30
  */
31
- debug?: boolean
32
-
33
- // logStatusChange?: boolean
34
- // logSizeChange?: boolean
35
-
36
- // timeout
31
+ logLevel?: CommonLogLevel
37
32
 
38
33
  /**
39
34
  * By default .push method resolves when the Promise is done (finished).
@@ -46,12 +41,6 @@ export interface PQueueCfg {
46
41
  resolveOn?: 'finish' | 'start'
47
42
  }
48
43
 
49
- export type PromiseReturningFunction<R> = () => Promise<R>
50
-
51
- interface PromiseReturningFunctionWithDefer<R> extends PromiseReturningFunction<R> {
52
- defer: DeferredPromise<R>
53
- }
54
-
55
44
  /**
56
45
  * Inspired by: https://github.com/sindresorhus/p-queue
57
46
  *
@@ -63,81 +52,60 @@ interface PromiseReturningFunctionWithDefer<R> extends PromiseReturningFunction<
63
52
  export class PQueue {
64
53
  constructor(cfg: PQueueCfg) {
65
54
  this.cfg = {
66
- // concurrency: Number.MAX_SAFE_INTEGER,
67
55
  errorMode: ErrorMode.THROW_IMMEDIATELY,
68
- logger: console,
69
- debug: false,
70
- resolveOn: 'finish',
71
56
  ...cfg,
72
57
  }
73
-
74
- if (!cfg.debug) {
75
- this.debug = () => {}
76
- }
58
+ this.logger = createCommonLoggerAtLevel(cfg.logger, cfg.logLevel)
59
+ this.resolveOnStart = this.cfg.resolveOn === 'start'
77
60
  }
78
61
 
79
- private readonly cfg: Required<PQueueCfg>
80
-
81
- private debug(...args: any[]): void {
82
- this.cfg.logger.log(...args)
83
- }
62
+ private readonly cfg: PQueueCfg
63
+ private readonly resolveOnStart: boolean
64
+ private readonly logger: CommonLogger
84
65
 
85
66
  inFlight = 0
86
- private queue: PromiseReturningFunction<any>[] = []
67
+ private queue: AsyncFunction[] = []
87
68
  private onIdleListeners: DeferredPromise[] = []
88
69
 
89
- get queueSize(): number {
90
- return this.queue.length
91
- }
92
-
93
- /**
94
- * Returns a Promise that resolves when the queue is Idle (next time, since the call).
95
- * Resolves immediately in case the queue is Idle.
96
- * Idle means 0 queue and 0 inFlight.
97
- */
98
- async onIdle(): Promise<void> {
99
- if (this.queue.length === 0 && this.inFlight === 0) return
100
-
101
- const listener = pDefer()
102
- this.onIdleListeners.push(listener)
103
- return await listener
104
- }
105
-
106
70
  /**
107
71
  * Push PromiseReturningFunction to the Queue.
108
72
  * Returns a Promise that resolves (or rejects) with the return value from the Promise.
109
73
  */
110
- async push<R>(fn_: PromiseReturningFunction<R>): Promise<R> {
74
+ async push<R>(fn_: AsyncFunction<R>): Promise<R> {
111
75
  const { concurrency } = this.cfg
112
- const resolveOnStart = this.cfg.resolveOn === 'start'
76
+ const { resolveOnStart, logger } = this
113
77
 
114
- const fn = fn_ as PromiseReturningFunctionWithDefer<R>
78
+ const fn = fn_ as AsyncFunctionWithDefer<R>
115
79
  fn.defer ||= pDefer<R>()
116
80
 
117
81
  if (this.inFlight < concurrency) {
118
82
  // There is room for more jobs. Can start immediately
119
83
  this.inFlight++
120
- this.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
84
+ logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
121
85
  if (resolveOnStart) fn.defer.resolve()
122
86
 
123
- fn()
87
+ runSafe(fn)
124
88
  .then(result => {
125
89
  if (!resolveOnStart) fn.defer.resolve(result)
126
90
  })
127
91
  .catch((err: Error) => {
128
- this.cfg.logger.error(err)
129
- if (resolveOnStart) return
92
+ if (resolveOnStart) {
93
+ logger.error(err)
94
+ return
95
+ }
130
96
 
131
97
  if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
98
+ logger.error(err)
132
99
  fn.defer.resolve() // resolve with `void`
133
100
  } else {
134
101
  // Should be handled on the outside, otherwise it'll cause UnhandledRejection
102
+ // Not logging, because it's re-thrown upstream
135
103
  fn.defer.reject(err)
136
104
  }
137
105
  })
138
106
  .finally(() => {
139
107
  this.inFlight--
140
- this.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
108
+ logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
141
109
 
142
110
  // check if there's room to start next job
143
111
  if (this.queue.length && this.inFlight <= concurrency) {
@@ -145,7 +113,7 @@ export class PQueue {
145
113
  void this.push(nextFn)
146
114
  } else {
147
115
  if (this.inFlight === 0) {
148
- this.debug('onIdle')
116
+ logger.debug('onIdle')
149
117
  this.onIdleListeners.forEach(defer => defer.resolve())
150
118
  this.onIdleListeners.length = 0 // empty the array
151
119
  }
@@ -153,9 +121,42 @@ export class PQueue {
153
121
  })
154
122
  } else {
155
123
  this.queue.push(fn)
156
- this.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`)
124
+ logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`)
157
125
  }
158
126
 
159
127
  return await fn.defer
160
128
  }
129
+
130
+ get queueSize(): number {
131
+ return this.queue.length
132
+ }
133
+
134
+ /**
135
+ * Returns a Promise that resolves when the queue is Idle (next time, since the call).
136
+ * Resolves immediately in case the queue is Idle.
137
+ * Idle means 0 queue and 0 inFlight.
138
+ */
139
+ async onIdle(): Promise<void> {
140
+ if (this.queue.length === 0 && this.inFlight === 0) return
141
+
142
+ const listener = pDefer()
143
+ this.onIdleListeners.push(listener)
144
+ return await listener
145
+ }
146
+ }
147
+
148
+ // Here we intentionally want it not async, as we don't want it to throw
149
+ // oxlint-disable-next-line typescript/promise-function-async
150
+ function runSafe<R>(fn: AsyncFunction<R>): Promise<R> {
151
+ try {
152
+ // Here we are intentionally not awaiting
153
+ return fn()
154
+ } catch (err) {
155
+ // Handle synchronous throws - ensure inFlight is decremented
156
+ return Promise.reject(err as Error)
157
+ }
158
+ }
159
+
160
+ interface AsyncFunctionWithDefer<R> extends AsyncFunction<R> {
161
+ defer: DeferredPromise<R>
161
162
  }
package/src/types.ts CHANGED
@@ -526,4 +526,4 @@ type ReadonlyObjectDeep<ObjectType extends object> = {
526
526
  */
527
527
  export type RequiredProp<T, K extends keyof T> = Required<Pick<T, K>> & T
528
528
 
529
- export * from './typeFest.js'
529
+ export type * from './typeFest.js'