@mrjacket/ahko 0.6.0 → 1.1.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/CHANGELOG.md +59 -0
- package/README.md +142 -15
- package/dist/ahko.d.ts +80 -11
- package/dist/config/config-loader.d.ts +31 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/errors/circuit-breaker.error.d.ts +24 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/index.cjs +651 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +637 -84
- package/dist/index.js.map +1 -1
- package/dist/models/circuit-breaker.model.d.ts +39 -0
- package/dist/models/config.model.d.ts +23 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/options.model.d.ts +23 -0
- package/dist/models/priority.model.d.ts +18 -0
- package/dist/models/stats.model.d.ts +5 -0
- package/dist/scheduler/circuit-breaker.d.ts +56 -0
- package/dist/scheduler/debounce-coordinator.d.ts +2 -0
- package/dist/scheduler/task-queue.d.ts +29 -4
- package/dist/scheduler/task-runner.d.ts +9 -0
- package/dist/scheduler/throttle-coordinator.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/errors/ahko.error.ts
|
|
2
9
|
var AhkoError = class extends Error {
|
|
3
10
|
/**
|
|
@@ -28,6 +35,72 @@ var AhkoConfigurationError = class extends AhkoError {
|
|
|
28
35
|
}
|
|
29
36
|
};
|
|
30
37
|
|
|
38
|
+
// src/config/config-loader.ts
|
|
39
|
+
var activeConfig;
|
|
40
|
+
function loadConfig(config) {
|
|
41
|
+
activeConfig = { ...config };
|
|
42
|
+
}
|
|
43
|
+
function resetConfig() {
|
|
44
|
+
activeConfig = void 0;
|
|
45
|
+
}
|
|
46
|
+
async function loadConfigFile(filePath = "config.ahko.json") {
|
|
47
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const { readFile } = await import("fs/promises");
|
|
52
|
+
const { resolve } = await import("path");
|
|
53
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
54
|
+
const content = await readFile(resolvedPath, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(content);
|
|
56
|
+
activeConfig = parsed;
|
|
57
|
+
return parsed;
|
|
58
|
+
} catch {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function tryAutoDiscoverSync() {
|
|
63
|
+
if (activeConfig !== void 0 || typeof process === "undefined" || !process.versions?.node) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
let fs = null;
|
|
68
|
+
let path = null;
|
|
69
|
+
if (typeof process.getBuiltinModule === "function") {
|
|
70
|
+
const getBuiltin = process.getBuiltinModule;
|
|
71
|
+
fs = getBuiltin("node:fs");
|
|
72
|
+
path = getBuiltin("node:path");
|
|
73
|
+
} else if (typeof __require === "function") {
|
|
74
|
+
fs = __require("fs");
|
|
75
|
+
path = __require("path");
|
|
76
|
+
}
|
|
77
|
+
if (fs && path) {
|
|
78
|
+
const configPath = path.resolve(process.cwd(), "config.ahko.json");
|
|
79
|
+
if (fs.existsSync(configPath)) {
|
|
80
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
81
|
+
activeConfig = JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function getActiveConfig() {
|
|
88
|
+
if (activeConfig === void 0) {
|
|
89
|
+
tryAutoDiscoverSync();
|
|
90
|
+
}
|
|
91
|
+
return activeConfig;
|
|
92
|
+
}
|
|
93
|
+
function getProfileConfig(profileName) {
|
|
94
|
+
const config = getActiveConfig();
|
|
95
|
+
if (!config) {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
if (profileName) {
|
|
99
|
+
return config.profiles?.[profileName];
|
|
100
|
+
}
|
|
101
|
+
return config.default;
|
|
102
|
+
}
|
|
103
|
+
|
|
31
104
|
// src/models/strategy.model.ts
|
|
32
105
|
var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
33
106
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
@@ -38,6 +111,23 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
38
111
|
return EScheduleStrategy2;
|
|
39
112
|
})(EScheduleStrategy || {});
|
|
40
113
|
|
|
114
|
+
// src/errors/circuit-breaker.error.ts
|
|
115
|
+
var AhkoCircuitBreakerOpenError = class extends AhkoError {
|
|
116
|
+
/** Time remaining in milliseconds before trial execution is allowed */
|
|
117
|
+
resetTimeoutMs;
|
|
118
|
+
/** Timestamp when the circuit tripped open */
|
|
119
|
+
trippedAt;
|
|
120
|
+
/** Total consecutive failures that caused the trip */
|
|
121
|
+
consecutiveFailures;
|
|
122
|
+
constructor(message = "Circuit breaker is open. Fast-failing task execution to protect downstream resources.", options) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.name = "AhkoCircuitBreakerOpenError";
|
|
125
|
+
this.resetTimeoutMs = options?.resetTimeoutMs;
|
|
126
|
+
this.trippedAt = options?.trippedAt;
|
|
127
|
+
this.consecutiveFailures = options?.consecutiveFailures;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
41
131
|
// src/errors/timeout.error.ts
|
|
42
132
|
var AhkoTimeoutError = class extends AhkoError {
|
|
43
133
|
/**
|
|
@@ -58,6 +148,28 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
58
148
|
}
|
|
59
149
|
};
|
|
60
150
|
|
|
151
|
+
// src/models/priority.model.ts
|
|
152
|
+
var TASK_PRIORITY_WEIGHTS = {
|
|
153
|
+
high: 10,
|
|
154
|
+
normal: 0,
|
|
155
|
+
low: -10
|
|
156
|
+
};
|
|
157
|
+
function resolvePriorityWeight(priority) {
|
|
158
|
+
if (priority === void 0) {
|
|
159
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
160
|
+
}
|
|
161
|
+
if (typeof priority === "number") {
|
|
162
|
+
return Number.isFinite(priority) ? priority : TASK_PRIORITY_WEIGHTS.normal;
|
|
163
|
+
}
|
|
164
|
+
if (priority === "high") {
|
|
165
|
+
return TASK_PRIORITY_WEIGHTS.high;
|
|
166
|
+
}
|
|
167
|
+
if (priority === "low") {
|
|
168
|
+
return TASK_PRIORITY_WEIGHTS.low;
|
|
169
|
+
}
|
|
170
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
171
|
+
}
|
|
172
|
+
|
|
61
173
|
// src/models/state.model.ts
|
|
62
174
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
63
175
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -94,6 +206,123 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
94
206
|
return Math.floor(cappedDelay);
|
|
95
207
|
}
|
|
96
208
|
|
|
209
|
+
// src/models/circuit-breaker.model.ts
|
|
210
|
+
var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
|
|
211
|
+
ECircuitState2["CLOSED"] = "closed";
|
|
212
|
+
ECircuitState2["OPEN"] = "open";
|
|
213
|
+
ECircuitState2["HALF_OPEN"] = "half_open";
|
|
214
|
+
return ECircuitState2;
|
|
215
|
+
})(ECircuitState || {});
|
|
216
|
+
|
|
217
|
+
// src/scheduler/circuit-breaker.ts
|
|
218
|
+
var CircuitBreakerCoordinator = class {
|
|
219
|
+
_state = "closed" /* CLOSED */;
|
|
220
|
+
_consecutiveFailures = 0;
|
|
221
|
+
_lastFailureTime;
|
|
222
|
+
failureThreshold;
|
|
223
|
+
resetTimeoutMs;
|
|
224
|
+
/**
|
|
225
|
+
* Initializes a new CircuitBreakerCoordinator.
|
|
226
|
+
*
|
|
227
|
+
* @param options - Configuration options for threshold and cool-down window.
|
|
228
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
229
|
+
*/
|
|
230
|
+
constructor(options) {
|
|
231
|
+
if (typeof options.failureThreshold !== "number" || Number.isNaN(options.failureThreshold) || !Number.isInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
232
|
+
throw new AhkoConfigurationError(
|
|
233
|
+
`Invalid failureThreshold "${options.failureThreshold}". failureThreshold must be an integer greater than or equal to 1.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (typeof options.resetTimeoutMs !== "number" || Number.isNaN(options.resetTimeoutMs) || !Number.isFinite(options.resetTimeoutMs) || options.resetTimeoutMs <= 0) {
|
|
237
|
+
throw new AhkoConfigurationError(
|
|
238
|
+
`Invalid resetTimeoutMs "${options.resetTimeoutMs}". resetTimeoutMs must be a positive finite number greater than 0.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
this.failureThreshold = options.failureThreshold;
|
|
242
|
+
this.resetTimeoutMs = options.resetTimeoutMs;
|
|
243
|
+
}
|
|
244
|
+
/** Current state of the circuit breaker */
|
|
245
|
+
get state() {
|
|
246
|
+
this.refreshState();
|
|
247
|
+
return this._state;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Checks whether an execution is currently allowed.
|
|
251
|
+
* If the circuit is OPEN and cool-down has not elapsed, fast-fails immediately.
|
|
252
|
+
*
|
|
253
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit is currently OPEN.
|
|
254
|
+
*/
|
|
255
|
+
checkAllowed() {
|
|
256
|
+
this.refreshState();
|
|
257
|
+
if (this._state === "open" /* OPEN */) {
|
|
258
|
+
const remainingMs = this._lastFailureTime ? Math.max(0, this.resetTimeoutMs - (Date.now() - this._lastFailureTime)) : this.resetTimeoutMs;
|
|
259
|
+
throw new AhkoCircuitBreakerOpenError(
|
|
260
|
+
`Circuit breaker is open. Fast-failing task execution. Remaining cool-down: ${remainingMs}ms.`,
|
|
261
|
+
{
|
|
262
|
+
resetTimeoutMs: remainingMs,
|
|
263
|
+
trippedAt: this._lastFailureTime,
|
|
264
|
+
consecutiveFailures: this._consecutiveFailures
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Records a successful task execution.
|
|
271
|
+
* Heals HALF_OPEN state back to CLOSED and resets consecutive failure counters.
|
|
272
|
+
*/
|
|
273
|
+
recordSuccess() {
|
|
274
|
+
this._consecutiveFailures = 0;
|
|
275
|
+
this._state = "closed" /* CLOSED */;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Records a failed task execution.
|
|
279
|
+
* Trips CLOSED to OPEN when threshold is met, or re-trips HALF_OPEN immediately.
|
|
280
|
+
*
|
|
281
|
+
* @param _error - Optional error that caused the failure.
|
|
282
|
+
*/
|
|
283
|
+
recordFailure(_error) {
|
|
284
|
+
this._consecutiveFailures++;
|
|
285
|
+
this._lastFailureTime = Date.now();
|
|
286
|
+
if (this._state === "half_open" /* HALF_OPEN */) {
|
|
287
|
+
this._state = "open" /* OPEN */;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (this._consecutiveFailures >= this.failureThreshold) {
|
|
291
|
+
this._state = "open" /* OPEN */;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Evaluates if enough time has passed to transition from OPEN to HALF_OPEN.
|
|
296
|
+
*/
|
|
297
|
+
refreshState() {
|
|
298
|
+
if (this._state === "open" /* OPEN */ && this._lastFailureTime !== void 0) {
|
|
299
|
+
const elapsed = Date.now() - this._lastFailureTime;
|
|
300
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
301
|
+
this._state = "half_open" /* HALF_OPEN */;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Resets the circuit breaker back to initial CLOSED state.
|
|
307
|
+
*/
|
|
308
|
+
reset() {
|
|
309
|
+
this._state = "closed" /* CLOSED */;
|
|
310
|
+
this._consecutiveFailures = 0;
|
|
311
|
+
this._lastFailureTime = void 0;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Returns a snapshot of circuit breaker telemetry.
|
|
315
|
+
*/
|
|
316
|
+
getStats() {
|
|
317
|
+
this.refreshState();
|
|
318
|
+
return {
|
|
319
|
+
state: this._state,
|
|
320
|
+
consecutiveFailures: this._consecutiveFailures,
|
|
321
|
+
lastFailureTime: this._lastFailureTime
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
97
326
|
// src/errors/cancellation.error.ts
|
|
98
327
|
var AhkoCancellationError = class extends AhkoError {
|
|
99
328
|
/**
|
|
@@ -112,6 +341,8 @@ var AhkoCancellationError = class extends AhkoError {
|
|
|
112
341
|
// src/scheduler/debounce-coordinator.ts
|
|
113
342
|
var DebounceCoordinator = class {
|
|
114
343
|
entries = /* @__PURE__ */ new Map();
|
|
344
|
+
/** Optional callback invoked whenever entries are settled or removed from coordinator */
|
|
345
|
+
onSettled;
|
|
115
346
|
/**
|
|
116
347
|
* Schedules a task under the debounce strategy.
|
|
117
348
|
*
|
|
@@ -198,6 +429,7 @@ var DebounceCoordinator = class {
|
|
|
198
429
|
return;
|
|
199
430
|
}
|
|
200
431
|
this.entries.delete(key);
|
|
432
|
+
this.onSettled?.();
|
|
201
433
|
if (entry.options?.signal && entry.abortListener) {
|
|
202
434
|
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
203
435
|
}
|
|
@@ -221,6 +453,7 @@ var DebounceCoordinator = class {
|
|
|
221
453
|
}
|
|
222
454
|
clearTimeout(entry.timerId);
|
|
223
455
|
this.entries.delete(key);
|
|
456
|
+
this.onSettled?.();
|
|
224
457
|
if (entry.options?.signal && entry.abortListener) {
|
|
225
458
|
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
226
459
|
}
|
|
@@ -248,6 +481,7 @@ var DebounceCoordinator = class {
|
|
|
248
481
|
entry.reject(new AhkoCancellationError("Debounced tasks cleared"));
|
|
249
482
|
}
|
|
250
483
|
this.entries.clear();
|
|
484
|
+
this.onSettled?.();
|
|
251
485
|
}
|
|
252
486
|
};
|
|
253
487
|
|
|
@@ -293,6 +527,8 @@ var IdleScheduler = class {
|
|
|
293
527
|
// src/scheduler/throttle-coordinator.ts
|
|
294
528
|
var ThrottleCoordinator = class {
|
|
295
529
|
entries = /* @__PURE__ */ new Map();
|
|
530
|
+
/** Optional callback invoked whenever entries are settled or removed from coordinator */
|
|
531
|
+
onSettled;
|
|
296
532
|
/**
|
|
297
533
|
* Schedules a task under the throttle strategy.
|
|
298
534
|
*
|
|
@@ -378,6 +614,7 @@ var ThrottleCoordinator = class {
|
|
|
378
614
|
return;
|
|
379
615
|
}
|
|
380
616
|
this.entries.delete(key);
|
|
617
|
+
this.onSettled?.();
|
|
381
618
|
}
|
|
382
619
|
/**
|
|
383
620
|
* Cancels any pending trailing throttled task for a given key.
|
|
@@ -394,6 +631,7 @@ var ThrottleCoordinator = class {
|
|
|
394
631
|
clearTimeout(entry.windowTimerId);
|
|
395
632
|
}
|
|
396
633
|
this.entries.delete(key);
|
|
634
|
+
this.onSettled?.();
|
|
397
635
|
if (entry.trailingReject) {
|
|
398
636
|
const cancelError = new AhkoCancellationError(
|
|
399
637
|
typeof reason === "string" ? reason : "Throttled task was cancelled",
|
|
@@ -421,6 +659,7 @@ var ThrottleCoordinator = class {
|
|
|
421
659
|
}
|
|
422
660
|
}
|
|
423
661
|
this.entries.clear();
|
|
662
|
+
this.onSettled?.();
|
|
424
663
|
}
|
|
425
664
|
};
|
|
426
665
|
|
|
@@ -518,6 +757,10 @@ var TaskQueue = class {
|
|
|
518
757
|
throttleCoordinator = new ThrottleCoordinator();
|
|
519
758
|
/** Lifecycle event emitter for task and scheduler events */
|
|
520
759
|
emitter = new AhkoEventEmitter();
|
|
760
|
+
/** Circuit breaker coordinator if configured */
|
|
761
|
+
circuitBreakerCoordinator;
|
|
762
|
+
/** Pause state flag */
|
|
763
|
+
_isPaused = false;
|
|
521
764
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
522
765
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
523
766
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -539,9 +782,10 @@ var TaskQueue = class {
|
|
|
539
782
|
*
|
|
540
783
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
541
784
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
785
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
542
786
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
543
787
|
*/
|
|
544
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
788
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
|
|
545
789
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
546
790
|
throw new AhkoConfigurationError(
|
|
547
791
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -554,6 +798,50 @@ var TaskQueue = class {
|
|
|
554
798
|
}
|
|
555
799
|
this.concurrency = concurrency;
|
|
556
800
|
this.minIntervalMs = minIntervalMs;
|
|
801
|
+
if (circuitBreakerOptions) {
|
|
802
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
803
|
+
}
|
|
804
|
+
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
805
|
+
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
|
|
809
|
+
*/
|
|
810
|
+
pause() {
|
|
811
|
+
this._isPaused = true;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Resumes queue execution, immediately dispatching waiting tasks up to available concurrency.
|
|
815
|
+
*/
|
|
816
|
+
resume() {
|
|
817
|
+
if (this._isPaused) {
|
|
818
|
+
this._isPaused = false;
|
|
819
|
+
this.pump();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Checks whether the task queue is currently paused.
|
|
824
|
+
*/
|
|
825
|
+
isPaused() {
|
|
826
|
+
return this._isPaused;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Inserts a task runner into the queue based on priority weight (descending).
|
|
830
|
+
* Preserves FIFO ordering among tasks with identical priority.
|
|
831
|
+
*/
|
|
832
|
+
insertIntoQueue(runner) {
|
|
833
|
+
const options = this.runnerOptions.get(runner);
|
|
834
|
+
const targetWeight = resolvePriorityWeight(options?.priority);
|
|
835
|
+
let insertIndex = this.queue.length;
|
|
836
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
837
|
+
const existingOptions = this.runnerOptions.get(this.queue[i]);
|
|
838
|
+
const existingWeight = resolvePriorityWeight(existingOptions?.priority);
|
|
839
|
+
if (existingWeight < targetWeight) {
|
|
840
|
+
insertIndex = i;
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
this.queue.splice(insertIndex, 0, runner);
|
|
557
845
|
}
|
|
558
846
|
/**
|
|
559
847
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -608,6 +896,13 @@ var TaskQueue = class {
|
|
|
608
896
|
);
|
|
609
897
|
}
|
|
610
898
|
}
|
|
899
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
900
|
+
if (typeof options.totalTimeoutMs !== "number" || Number.isNaN(options.totalTimeoutMs) || !Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0) {
|
|
901
|
+
throw new AhkoConfigurationError(
|
|
902
|
+
`Invalid totalTimeoutMs "${options.totalTimeoutMs}". totalTimeoutMs must be a positive finite number greater than 0.`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
611
906
|
if (options) {
|
|
612
907
|
this.runnerOptions.set(runner, options);
|
|
613
908
|
}
|
|
@@ -615,6 +910,16 @@ var TaskQueue = class {
|
|
|
615
910
|
this.cancelledTasks++;
|
|
616
911
|
return runner.promise;
|
|
617
912
|
}
|
|
913
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
914
|
+
const budgetMs = options.totalTimeoutMs;
|
|
915
|
+
const totalTimerId = setTimeout(() => {
|
|
916
|
+
runner.timeout(budgetMs, `Task total execution deadline exceeded after ${budgetMs}ms`);
|
|
917
|
+
}, budgetMs);
|
|
918
|
+
runner.promise.finally(() => {
|
|
919
|
+
clearTimeout(totalTimerId);
|
|
920
|
+
}).catch(() => {
|
|
921
|
+
});
|
|
922
|
+
}
|
|
618
923
|
if (strategy === "delay" /* DELAY */) {
|
|
619
924
|
const delayMs = options?.delay ?? 0;
|
|
620
925
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -638,15 +943,23 @@ var TaskQueue = class {
|
|
|
638
943
|
const index = this.queue.indexOf(runner);
|
|
639
944
|
if (index !== -1) {
|
|
640
945
|
this.queue.splice(index, 1);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
946
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
947
|
+
this.timedOutTasks++;
|
|
948
|
+
this.emitter.emit("task:timeout", {
|
|
949
|
+
taskId: runner.taskId,
|
|
950
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
951
|
+
});
|
|
952
|
+
} else {
|
|
953
|
+
this.cancelledTasks++;
|
|
954
|
+
this.emitter.emit("task:cancel", {
|
|
955
|
+
taskId: runner.taskId,
|
|
956
|
+
reason: "Task cancelled while queued"
|
|
957
|
+
});
|
|
958
|
+
}
|
|
646
959
|
this.checkIdle();
|
|
647
960
|
}
|
|
648
961
|
};
|
|
649
|
-
this.
|
|
962
|
+
this.insertIntoQueue(runner);
|
|
650
963
|
this.pump();
|
|
651
964
|
return runner.promise;
|
|
652
965
|
}
|
|
@@ -659,22 +972,30 @@ var TaskQueue = class {
|
|
|
659
972
|
runner,
|
|
660
973
|
timerId: setTimeout(() => {
|
|
661
974
|
this.delayedEntries.delete(delayedEntry);
|
|
662
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
975
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
663
976
|
return;
|
|
664
977
|
}
|
|
665
978
|
runner.onCancel = () => {
|
|
666
979
|
const index = this.queue.indexOf(runner);
|
|
667
980
|
if (index !== -1) {
|
|
668
981
|
this.queue.splice(index, 1);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
982
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
983
|
+
this.timedOutTasks++;
|
|
984
|
+
this.emitter.emit("task:timeout", {
|
|
985
|
+
taskId: runner.taskId,
|
|
986
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
987
|
+
});
|
|
988
|
+
} else {
|
|
989
|
+
this.cancelledTasks++;
|
|
990
|
+
this.emitter.emit("task:cancel", {
|
|
991
|
+
taskId: runner.taskId,
|
|
992
|
+
reason: "Task cancelled while queued"
|
|
993
|
+
});
|
|
994
|
+
}
|
|
674
995
|
this.checkIdle();
|
|
675
996
|
}
|
|
676
997
|
};
|
|
677
|
-
this.
|
|
998
|
+
this.insertIntoQueue(runner);
|
|
678
999
|
this.pump();
|
|
679
1000
|
}, delayMs)
|
|
680
1001
|
};
|
|
@@ -683,11 +1004,19 @@ var TaskQueue = class {
|
|
|
683
1004
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
684
1005
|
clearTimeout(delayedEntry.timerId);
|
|
685
1006
|
this.delayedEntries.delete(delayedEntry);
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1007
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1008
|
+
this.timedOutTasks++;
|
|
1009
|
+
this.emitter.emit("task:timeout", {
|
|
1010
|
+
taskId: runner.taskId,
|
|
1011
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1012
|
+
});
|
|
1013
|
+
} else {
|
|
1014
|
+
this.cancelledTasks++;
|
|
1015
|
+
this.emitter.emit("task:cancel", {
|
|
1016
|
+
taskId: runner.taskId,
|
|
1017
|
+
reason: "Task cancelled while waiting in delay"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
691
1020
|
this.checkIdle();
|
|
692
1021
|
}
|
|
693
1022
|
};
|
|
@@ -700,22 +1029,30 @@ var TaskQueue = class {
|
|
|
700
1029
|
let idleEntry;
|
|
701
1030
|
const handle = IdleScheduler.schedule(() => {
|
|
702
1031
|
this.idleEntries.delete(idleEntry);
|
|
703
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1032
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
704
1033
|
return;
|
|
705
1034
|
}
|
|
706
1035
|
runner.onCancel = () => {
|
|
707
1036
|
const index = this.queue.indexOf(runner);
|
|
708
1037
|
if (index !== -1) {
|
|
709
1038
|
this.queue.splice(index, 1);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1039
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1040
|
+
this.timedOutTasks++;
|
|
1041
|
+
this.emitter.emit("task:timeout", {
|
|
1042
|
+
taskId: runner.taskId,
|
|
1043
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1044
|
+
});
|
|
1045
|
+
} else {
|
|
1046
|
+
this.cancelledTasks++;
|
|
1047
|
+
this.emitter.emit("task:cancel", {
|
|
1048
|
+
taskId: runner.taskId,
|
|
1049
|
+
reason: "Task cancelled while queued"
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
715
1052
|
this.checkIdle();
|
|
716
1053
|
}
|
|
717
1054
|
};
|
|
718
|
-
this.
|
|
1055
|
+
this.insertIntoQueue(runner);
|
|
719
1056
|
this.pump();
|
|
720
1057
|
}, idleTimeout);
|
|
721
1058
|
idleEntry = { runner, handle };
|
|
@@ -724,21 +1061,30 @@ var TaskQueue = class {
|
|
|
724
1061
|
if (this.idleEntries.has(idleEntry)) {
|
|
725
1062
|
handle.cancel();
|
|
726
1063
|
this.idleEntries.delete(idleEntry);
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1064
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1065
|
+
this.timedOutTasks++;
|
|
1066
|
+
this.emitter.emit("task:timeout", {
|
|
1067
|
+
taskId: runner.taskId,
|
|
1068
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1069
|
+
});
|
|
1070
|
+
} else {
|
|
1071
|
+
this.cancelledTasks++;
|
|
1072
|
+
this.emitter.emit("task:cancel", {
|
|
1073
|
+
taskId: runner.taskId,
|
|
1074
|
+
reason: "Task cancelled while waiting for idle"
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
732
1077
|
this.checkIdle();
|
|
733
1078
|
}
|
|
734
1079
|
};
|
|
735
1080
|
}
|
|
736
1081
|
/**
|
|
737
1082
|
* Pumps the queue by picking pending tasks and executing them
|
|
738
|
-
* as long as concurrency capacity is available
|
|
1083
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1084
|
+
* and queue is not paused.
|
|
739
1085
|
*/
|
|
740
1086
|
pump() {
|
|
741
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
1087
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
742
1088
|
return;
|
|
743
1089
|
}
|
|
744
1090
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -755,7 +1101,7 @@ var TaskQueue = class {
|
|
|
755
1101
|
return;
|
|
756
1102
|
}
|
|
757
1103
|
}
|
|
758
|
-
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
1104
|
+
while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
759
1105
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
760
1106
|
const now = Date.now();
|
|
761
1107
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -774,9 +1120,25 @@ var TaskQueue = class {
|
|
|
774
1120
|
if (!runner) {
|
|
775
1121
|
break;
|
|
776
1122
|
}
|
|
777
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1123
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
778
1124
|
continue;
|
|
779
1125
|
}
|
|
1126
|
+
if (this.circuitBreakerCoordinator) {
|
|
1127
|
+
try {
|
|
1128
|
+
this.circuitBreakerCoordinator.checkAllowed();
|
|
1129
|
+
} catch (cbError) {
|
|
1130
|
+
this.failedTasks++;
|
|
1131
|
+
this.runnerOptions.delete(runner);
|
|
1132
|
+
this.emitter.emit("task:fail", {
|
|
1133
|
+
taskId: runner.taskId,
|
|
1134
|
+
attempt: runner.attempt,
|
|
1135
|
+
error: cbError,
|
|
1136
|
+
willRetry: false
|
|
1137
|
+
});
|
|
1138
|
+
runner.reject(cbError);
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
780
1142
|
this.activeRunners.add(runner);
|
|
781
1143
|
this.lastTaskStartTime = Date.now();
|
|
782
1144
|
void this.executeRunner(runner);
|
|
@@ -805,6 +1167,7 @@ var TaskQueue = class {
|
|
|
805
1167
|
});
|
|
806
1168
|
try {
|
|
807
1169
|
const result = await runner.run();
|
|
1170
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
808
1171
|
this.completedTasks++;
|
|
809
1172
|
this.activeRunners.delete(runner);
|
|
810
1173
|
this.runnerOptions.delete(runner);
|
|
@@ -840,11 +1203,14 @@ var TaskQueue = class {
|
|
|
840
1203
|
this.scheduleRetry(runner, options);
|
|
841
1204
|
return;
|
|
842
1205
|
}
|
|
1206
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1207
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1208
|
+
}
|
|
843
1209
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
844
1210
|
this.timedOutTasks++;
|
|
845
1211
|
this.emitter.emit("task:timeout", {
|
|
846
1212
|
taskId: runner.taskId,
|
|
847
|
-
timeoutMs: runner.timeoutMs
|
|
1213
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
848
1214
|
});
|
|
849
1215
|
} else {
|
|
850
1216
|
this.failedTasks++;
|
|
@@ -874,15 +1240,23 @@ var TaskQueue = class {
|
|
|
874
1240
|
const index = this.queue.indexOf(runner);
|
|
875
1241
|
if (index !== -1) {
|
|
876
1242
|
this.queue.splice(index, 1);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1243
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1244
|
+
this.timedOutTasks++;
|
|
1245
|
+
this.emitter.emit("task:timeout", {
|
|
1246
|
+
taskId: runner.taskId,
|
|
1247
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1248
|
+
});
|
|
1249
|
+
} else {
|
|
1250
|
+
this.cancelledTasks++;
|
|
1251
|
+
this.emitter.emit("task:cancel", {
|
|
1252
|
+
taskId: runner.taskId,
|
|
1253
|
+
reason: "Task cancelled while queued"
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
882
1256
|
this.checkIdle();
|
|
883
1257
|
}
|
|
884
1258
|
};
|
|
885
|
-
this.
|
|
1259
|
+
this.insertIntoQueue(runner);
|
|
886
1260
|
this.pump();
|
|
887
1261
|
return;
|
|
888
1262
|
}
|
|
@@ -890,22 +1264,30 @@ var TaskQueue = class {
|
|
|
890
1264
|
runner,
|
|
891
1265
|
timerId: setTimeout(() => {
|
|
892
1266
|
this.retryEntries.delete(retryEntry);
|
|
893
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1267
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
894
1268
|
return;
|
|
895
1269
|
}
|
|
896
1270
|
runner.onCancel = () => {
|
|
897
1271
|
const index = this.queue.indexOf(runner);
|
|
898
1272
|
if (index !== -1) {
|
|
899
1273
|
this.queue.splice(index, 1);
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1274
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1275
|
+
this.timedOutTasks++;
|
|
1276
|
+
this.emitter.emit("task:timeout", {
|
|
1277
|
+
taskId: runner.taskId,
|
|
1278
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1279
|
+
});
|
|
1280
|
+
} else {
|
|
1281
|
+
this.cancelledTasks++;
|
|
1282
|
+
this.emitter.emit("task:cancel", {
|
|
1283
|
+
taskId: runner.taskId,
|
|
1284
|
+
reason: "Task cancelled while queued"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
905
1287
|
this.checkIdle();
|
|
906
1288
|
}
|
|
907
1289
|
};
|
|
908
|
-
this.
|
|
1290
|
+
this.insertIntoQueue(runner);
|
|
909
1291
|
this.pump();
|
|
910
1292
|
}, backoffDelay)
|
|
911
1293
|
};
|
|
@@ -914,11 +1296,19 @@ var TaskQueue = class {
|
|
|
914
1296
|
if (this.retryEntries.has(retryEntry)) {
|
|
915
1297
|
clearTimeout(retryEntry.timerId);
|
|
916
1298
|
this.retryEntries.delete(retryEntry);
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1299
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1300
|
+
this.timedOutTasks++;
|
|
1301
|
+
this.emitter.emit("task:timeout", {
|
|
1302
|
+
taskId: runner.taskId,
|
|
1303
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1304
|
+
});
|
|
1305
|
+
} else {
|
|
1306
|
+
this.cancelledTasks++;
|
|
1307
|
+
this.emitter.emit("task:cancel", {
|
|
1308
|
+
taskId: runner.taskId,
|
|
1309
|
+
reason: "Task cancelled during retry backoff"
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
922
1312
|
this.checkIdle();
|
|
923
1313
|
}
|
|
924
1314
|
};
|
|
@@ -965,7 +1355,7 @@ var TaskQueue = class {
|
|
|
965
1355
|
clear() {
|
|
966
1356
|
while (this.queue.length > 0) {
|
|
967
1357
|
const runner = this.queue.shift();
|
|
968
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1358
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
969
1359
|
runner.cancel("Scheduler cleared");
|
|
970
1360
|
this.cancelledTasks++;
|
|
971
1361
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1015,7 +1405,9 @@ var TaskQueue = class {
|
|
|
1015
1405
|
timedOutTasks: this.timedOutTasks,
|
|
1016
1406
|
retriedTasks: this.retriedTasks,
|
|
1017
1407
|
totalDispatched: this.totalDispatched,
|
|
1018
|
-
capacity: this.concurrency
|
|
1408
|
+
capacity: this.concurrency,
|
|
1409
|
+
isPaused: this._isPaused,
|
|
1410
|
+
circuitState: this.circuitBreakerCoordinator?.state
|
|
1019
1411
|
});
|
|
1020
1412
|
}
|
|
1021
1413
|
};
|
|
@@ -1086,6 +1478,8 @@ var TaskRunner = class {
|
|
|
1086
1478
|
}
|
|
1087
1479
|
}
|
|
1088
1480
|
}
|
|
1481
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1482
|
+
totalTimedOut = false;
|
|
1089
1483
|
/**
|
|
1090
1484
|
* Gets the current lifecycle state of the task.
|
|
1091
1485
|
*/
|
|
@@ -1118,7 +1512,7 @@ var TaskRunner = class {
|
|
|
1118
1512
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1119
1513
|
*/
|
|
1120
1514
|
async canRetry(error, retryOptions) {
|
|
1121
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1515
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1122
1516
|
return false;
|
|
1123
1517
|
}
|
|
1124
1518
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1159,15 +1553,16 @@ var TaskRunner = class {
|
|
|
1159
1553
|
let abortListener;
|
|
1160
1554
|
const abortPromise = new Promise((_, reject) => {
|
|
1161
1555
|
abortListener = () => {
|
|
1162
|
-
|
|
1556
|
+
const reason = this.abortController.signal.reason;
|
|
1557
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || reason instanceof AhkoTimeoutError) {
|
|
1558
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1163
1559
|
reject(
|
|
1164
|
-
new AhkoTimeoutError(
|
|
1560
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1165
1561
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1166
1562
|
{ timeoutMs: this.timeoutMs }
|
|
1167
1563
|
)
|
|
1168
1564
|
);
|
|
1169
1565
|
} else {
|
|
1170
|
-
const reason = this.abortController.signal.reason;
|
|
1171
1566
|
reject(
|
|
1172
1567
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1173
1568
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1202,6 +1597,10 @@ var TaskRunner = class {
|
|
|
1202
1597
|
}
|
|
1203
1598
|
taskExecutionPromise.catch(() => {
|
|
1204
1599
|
});
|
|
1600
|
+
abortPromise.catch(() => {
|
|
1601
|
+
});
|
|
1602
|
+
timeoutPromise?.catch(() => {
|
|
1603
|
+
});
|
|
1205
1604
|
const racePromises = [
|
|
1206
1605
|
taskExecutionPromise,
|
|
1207
1606
|
abortPromise
|
|
@@ -1293,6 +1692,31 @@ var TaskRunner = class {
|
|
|
1293
1692
|
this.onCancel?.(this);
|
|
1294
1693
|
}
|
|
1295
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Times out the task, aborting pending or running execution with AhkoTimeoutError.
|
|
1697
|
+
*
|
|
1698
|
+
* @param timeoutMs - Timeout duration in milliseconds.
|
|
1699
|
+
* @param message - Optional custom timeout message.
|
|
1700
|
+
*/
|
|
1701
|
+
timeout(timeoutMs, message) {
|
|
1702
|
+
if (this._state === "completed" /* COMPLETED */ || this._state === "failed" /* FAILED */ || this._state === "cancelled" /* CANCELLED */ || this._state === "timed_out" /* TIMED_OUT */) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const wasPending = this._state === "pending" /* PENDING */;
|
|
1706
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1707
|
+
this.totalTimedOut = true;
|
|
1708
|
+
this.clearTimeoutTimer();
|
|
1709
|
+
const timeoutError = new AhkoTimeoutError(
|
|
1710
|
+
message ?? `Task execution timed out after ${timeoutMs}ms`,
|
|
1711
|
+
{ timeoutMs }
|
|
1712
|
+
);
|
|
1713
|
+
this.abortController.abort(timeoutError);
|
|
1714
|
+
this.cleanup();
|
|
1715
|
+
if (wasPending) {
|
|
1716
|
+
this.rejectPromise(timeoutError);
|
|
1717
|
+
this.onCancel?.(this);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1296
1720
|
/**
|
|
1297
1721
|
* Handles external AbortSignal trigger.
|
|
1298
1722
|
*/
|
|
@@ -1311,9 +1735,55 @@ var TaskRunner = class {
|
|
|
1311
1735
|
};
|
|
1312
1736
|
|
|
1313
1737
|
// src/ahko.ts
|
|
1314
|
-
var Ahko = class {
|
|
1738
|
+
var Ahko = class _Ahko {
|
|
1315
1739
|
/** Internal queue and concurrency manager */
|
|
1316
1740
|
queue;
|
|
1741
|
+
/** Default schedule options inherited from profile if configured */
|
|
1742
|
+
defaultScheduleOptions;
|
|
1743
|
+
/**
|
|
1744
|
+
* Programmatically loads a declarative configuration into memory.
|
|
1745
|
+
* Works universally across Node.js, browsers, and edge runtimes.
|
|
1746
|
+
*
|
|
1747
|
+
* @param config - File configuration object containing default and named profiles.
|
|
1748
|
+
*/
|
|
1749
|
+
static loadConfig(config) {
|
|
1750
|
+
loadConfig(config);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Asynchronously loads a configuration file from disk (Node.js).
|
|
1754
|
+
*
|
|
1755
|
+
* @param filePath - Path to configuration file (default: "config.ahko.json").
|
|
1756
|
+
*/
|
|
1757
|
+
static async loadConfigFile(filePath) {
|
|
1758
|
+
return loadConfigFile(filePath);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Resets the active declarative configuration.
|
|
1762
|
+
*/
|
|
1763
|
+
static resetConfig() {
|
|
1764
|
+
resetConfig();
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Retrieves the currently active declarative configuration.
|
|
1768
|
+
*/
|
|
1769
|
+
static getActiveConfig() {
|
|
1770
|
+
return getActiveConfig();
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Instantiates an Ahko scheduler initialized with settings from a declarative profile.
|
|
1774
|
+
*
|
|
1775
|
+
* @param profileName - Optional name of the profile (e.g. "api", "background").
|
|
1776
|
+
* @param overrides - Optional scheduler options overriding profile values.
|
|
1777
|
+
* @returns A new configured Ahko instance.
|
|
1778
|
+
*/
|
|
1779
|
+
static fromProfile(profileName, overrides) {
|
|
1780
|
+
const profile = getProfileConfig(profileName);
|
|
1781
|
+
return new _Ahko({
|
|
1782
|
+
...profile,
|
|
1783
|
+
...overrides,
|
|
1784
|
+
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1317
1787
|
/**
|
|
1318
1788
|
* Initializes a new Ahko scheduler instance.
|
|
1319
1789
|
*
|
|
@@ -1326,79 +1796,152 @@ var Ahko = class {
|
|
|
1326
1796
|
* ```
|
|
1327
1797
|
*/
|
|
1328
1798
|
constructor(options) {
|
|
1329
|
-
|
|
1799
|
+
const profile = options?.profile ? getProfileConfig(options.profile) : getProfileConfig();
|
|
1800
|
+
const mergedOptions = {
|
|
1801
|
+
...profile,
|
|
1802
|
+
...options,
|
|
1803
|
+
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker
|
|
1804
|
+
};
|
|
1805
|
+
if (profile) {
|
|
1806
|
+
this.defaultScheduleOptions = {
|
|
1807
|
+
priority: profile.priority,
|
|
1808
|
+
retry: profile.retry,
|
|
1809
|
+
timeoutMs: profile.timeoutMs,
|
|
1810
|
+
totalTimeoutMs: profile.totalTimeoutMs
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
this.queue = new TaskQueue(
|
|
1814
|
+
mergedOptions.concurrency,
|
|
1815
|
+
mergedOptions.minIntervalMs,
|
|
1816
|
+
mergedOptions.circuitBreaker
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
|
|
1821
|
+
*/
|
|
1822
|
+
pause() {
|
|
1823
|
+
this.queue.pause();
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Resumes scheduler dispatch, immediately executing waiting tasks up to available concurrency.
|
|
1827
|
+
*/
|
|
1828
|
+
resume() {
|
|
1829
|
+
this.queue.resume();
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Checks whether the scheduler is currently paused.
|
|
1833
|
+
*/
|
|
1834
|
+
isPaused() {
|
|
1835
|
+
return this.queue.isPaused();
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Current circuit breaker state if circuit breaker protection is configured.
|
|
1839
|
+
*/
|
|
1840
|
+
get circuitState() {
|
|
1841
|
+
return this.queue.circuitBreakerCoordinator?.state;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Access to the underlying circuit breaker coordinator instance if configured.
|
|
1845
|
+
*/
|
|
1846
|
+
get circuitBreaker() {
|
|
1847
|
+
return this.queue.circuitBreakerCoordinator;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Wraps an async function so every execution is automatically routed through this Ahko scheduler.
|
|
1851
|
+
*
|
|
1852
|
+
* @template TArgs - Parameter types of the wrapped function.
|
|
1853
|
+
* @template TReturn - Return type of the wrapped function.
|
|
1854
|
+
* @param fn - The function to wrap.
|
|
1855
|
+
* @param options - Optional scheduling options applied to every wrapped call.
|
|
1856
|
+
* @returns A wrapped function returning a Promise.
|
|
1857
|
+
*
|
|
1858
|
+
* @example
|
|
1859
|
+
* ```typescript
|
|
1860
|
+
* const fetchUser = ahko.wrap(async (id: string) => api.getUser(id), { priority: "high" });
|
|
1861
|
+
* const user = await fetchUser("usr_123");
|
|
1862
|
+
* ```
|
|
1863
|
+
*/
|
|
1864
|
+
wrap(fn, options) {
|
|
1865
|
+
if (typeof fn !== "function") {
|
|
1866
|
+
throw new AhkoConfigurationError("Target to wrap must be a valid function.");
|
|
1867
|
+
}
|
|
1868
|
+
return (...args) => {
|
|
1869
|
+
return this.schedule(() => fn(...args), options);
|
|
1870
|
+
};
|
|
1330
1871
|
}
|
|
1331
1872
|
/**
|
|
1332
1873
|
* Schedules a task for execution with full return type inference.
|
|
1333
1874
|
*
|
|
1334
1875
|
* @template T - Inferred return type of the task.
|
|
1335
1876
|
* @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
|
|
1336
|
-
* @param options - Task-specific scheduling options such as strategy, delay, and cancellation signal.
|
|
1877
|
+
* @param options - Task-specific scheduling options such as strategy, priority, delay, and cancellation signal.
|
|
1337
1878
|
* @returns A promise that resolves with the task's return value.
|
|
1338
1879
|
*
|
|
1339
1880
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1340
1881
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1341
|
-
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
1882
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs or totalTimeoutMs.
|
|
1883
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit breaker is OPEN and rejects the execution.
|
|
1342
1884
|
*
|
|
1343
1885
|
* @example
|
|
1344
1886
|
* ```typescript
|
|
1345
1887
|
* // Immediate execution (subject to concurrency)
|
|
1346
1888
|
* const count = await ahko.schedule(async () => 42);
|
|
1347
1889
|
*
|
|
1348
|
-
* //
|
|
1349
|
-
* await ahko.schedule(
|
|
1350
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1351
|
-
* { strategy: "delay", delay: 1000 }
|
|
1352
|
-
* );
|
|
1890
|
+
* // High priority task
|
|
1891
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1353
1892
|
* ```
|
|
1354
1893
|
*/
|
|
1355
1894
|
schedule(task, options) {
|
|
1356
1895
|
if (typeof task !== "function") {
|
|
1357
1896
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1358
1897
|
}
|
|
1359
|
-
const
|
|
1898
|
+
const mergedOptions = {
|
|
1899
|
+
...this.defaultScheduleOptions,
|
|
1900
|
+
...options
|
|
1901
|
+
};
|
|
1902
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1360
1903
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1361
|
-
if (!
|
|
1904
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1362
1905
|
throw new AhkoConfigurationError(
|
|
1363
1906
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1364
1907
|
);
|
|
1365
1908
|
}
|
|
1366
|
-
const waitMs =
|
|
1909
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1367
1910
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1368
1911
|
throw new AhkoConfigurationError(
|
|
1369
1912
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1370
1913
|
);
|
|
1371
1914
|
}
|
|
1372
1915
|
return this.queue.debounceCoordinator.schedule(
|
|
1373
|
-
|
|
1916
|
+
mergedOptions.key,
|
|
1374
1917
|
task,
|
|
1375
1918
|
waitMs,
|
|
1376
|
-
|
|
1919
|
+
mergedOptions,
|
|
1377
1920
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1378
1921
|
);
|
|
1379
1922
|
}
|
|
1380
1923
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1381
|
-
if (!
|
|
1924
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1382
1925
|
throw new AhkoConfigurationError(
|
|
1383
1926
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1384
1927
|
);
|
|
1385
1928
|
}
|
|
1386
|
-
const waitMs =
|
|
1929
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1387
1930
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1388
1931
|
throw new AhkoConfigurationError(
|
|
1389
1932
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1390
1933
|
);
|
|
1391
1934
|
}
|
|
1392
1935
|
return this.queue.throttleCoordinator.schedule(
|
|
1393
|
-
|
|
1936
|
+
mergedOptions.key,
|
|
1394
1937
|
task,
|
|
1395
1938
|
waitMs,
|
|
1396
|
-
|
|
1939
|
+
mergedOptions,
|
|
1397
1940
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1398
1941
|
);
|
|
1399
1942
|
}
|
|
1400
|
-
const runner = new TaskRunner(task,
|
|
1401
|
-
return this.queue.enqueue(runner,
|
|
1943
|
+
const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
|
|
1944
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1402
1945
|
}
|
|
1403
1946
|
/**
|
|
1404
1947
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1461,12 +2004,12 @@ var Ahko = class {
|
|
|
1461
2004
|
/**
|
|
1462
2005
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1463
2006
|
*
|
|
1464
|
-
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled,
|
|
2007
|
+
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, timed out tasks, pause status, and circuit state.
|
|
1465
2008
|
*
|
|
1466
2009
|
* @example
|
|
1467
2010
|
* ```typescript
|
|
1468
2011
|
* const stats = ahko.stats();
|
|
1469
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2012
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1470
2013
|
* ```
|
|
1471
2014
|
*/
|
|
1472
2015
|
stats() {
|
|
@@ -1552,7 +2095,7 @@ var Ahko = class {
|
|
|
1552
2095
|
};
|
|
1553
2096
|
|
|
1554
2097
|
// src/version.ts
|
|
1555
|
-
var VERSION = "
|
|
2098
|
+
var VERSION = "1.1.0";
|
|
1556
2099
|
|
|
1557
2100
|
// src/errors/queue.error.ts
|
|
1558
2101
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1626,16 +2169,26 @@ function combineSignals(signals) {
|
|
|
1626
2169
|
export {
|
|
1627
2170
|
Ahko,
|
|
1628
2171
|
AhkoCancellationError,
|
|
2172
|
+
AhkoCircuitBreakerOpenError,
|
|
1629
2173
|
AhkoConfigurationError,
|
|
1630
2174
|
AhkoError,
|
|
1631
2175
|
AhkoQueueError,
|
|
1632
2176
|
AhkoTimeoutError,
|
|
2177
|
+
CircuitBreakerCoordinator,
|
|
1633
2178
|
DEFAULT_BASE_DELAY,
|
|
1634
2179
|
DEFAULT_MAX_DELAY,
|
|
2180
|
+
ECircuitState,
|
|
1635
2181
|
EScheduleStrategy,
|
|
1636
2182
|
ETaskState,
|
|
2183
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1637
2184
|
VERSION,
|
|
1638
2185
|
calculateBackoff,
|
|
1639
|
-
combineSignals
|
|
2186
|
+
combineSignals,
|
|
2187
|
+
getActiveConfig,
|
|
2188
|
+
getProfileConfig,
|
|
2189
|
+
loadConfig,
|
|
2190
|
+
loadConfigFile,
|
|
2191
|
+
resetConfig,
|
|
2192
|
+
resolvePriorityWeight
|
|
1640
2193
|
};
|
|
1641
2194
|
//# sourceMappingURL=index.js.map
|