@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.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,17 +32,27 @@ var src_exports = {};
|
|
|
22
32
|
__export(src_exports, {
|
|
23
33
|
Ahko: () => Ahko,
|
|
24
34
|
AhkoCancellationError: () => AhkoCancellationError,
|
|
35
|
+
AhkoCircuitBreakerOpenError: () => AhkoCircuitBreakerOpenError,
|
|
25
36
|
AhkoConfigurationError: () => AhkoConfigurationError,
|
|
26
37
|
AhkoError: () => AhkoError,
|
|
27
38
|
AhkoQueueError: () => AhkoQueueError,
|
|
28
39
|
AhkoTimeoutError: () => AhkoTimeoutError,
|
|
40
|
+
CircuitBreakerCoordinator: () => CircuitBreakerCoordinator,
|
|
29
41
|
DEFAULT_BASE_DELAY: () => DEFAULT_BASE_DELAY,
|
|
30
42
|
DEFAULT_MAX_DELAY: () => DEFAULT_MAX_DELAY,
|
|
43
|
+
ECircuitState: () => ECircuitState,
|
|
31
44
|
EScheduleStrategy: () => EScheduleStrategy,
|
|
32
45
|
ETaskState: () => ETaskState,
|
|
46
|
+
TASK_PRIORITY_WEIGHTS: () => TASK_PRIORITY_WEIGHTS,
|
|
33
47
|
VERSION: () => VERSION,
|
|
34
48
|
calculateBackoff: () => calculateBackoff,
|
|
35
|
-
combineSignals: () => combineSignals
|
|
49
|
+
combineSignals: () => combineSignals,
|
|
50
|
+
getActiveConfig: () => getActiveConfig,
|
|
51
|
+
getProfileConfig: () => getProfileConfig,
|
|
52
|
+
loadConfig: () => loadConfig,
|
|
53
|
+
loadConfigFile: () => loadConfigFile,
|
|
54
|
+
resetConfig: () => resetConfig,
|
|
55
|
+
resolvePriorityWeight: () => resolvePriorityWeight
|
|
36
56
|
});
|
|
37
57
|
module.exports = __toCommonJS(src_exports);
|
|
38
58
|
|
|
@@ -66,6 +86,72 @@ var AhkoConfigurationError = class extends AhkoError {
|
|
|
66
86
|
}
|
|
67
87
|
};
|
|
68
88
|
|
|
89
|
+
// src/config/config-loader.ts
|
|
90
|
+
var activeConfig;
|
|
91
|
+
function loadConfig(config) {
|
|
92
|
+
activeConfig = { ...config };
|
|
93
|
+
}
|
|
94
|
+
function resetConfig() {
|
|
95
|
+
activeConfig = void 0;
|
|
96
|
+
}
|
|
97
|
+
async function loadConfigFile(filePath = "config.ahko.json") {
|
|
98
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const { readFile } = await import("fs/promises");
|
|
103
|
+
const { resolve } = await import("path");
|
|
104
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
105
|
+
const content = await readFile(resolvedPath, "utf-8");
|
|
106
|
+
const parsed = JSON.parse(content);
|
|
107
|
+
activeConfig = parsed;
|
|
108
|
+
return parsed;
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function tryAutoDiscoverSync() {
|
|
114
|
+
if (activeConfig !== void 0 || typeof process === "undefined" || !process.versions?.node) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
let fs = null;
|
|
119
|
+
let path = null;
|
|
120
|
+
if (typeof process.getBuiltinModule === "function") {
|
|
121
|
+
const getBuiltin = process.getBuiltinModule;
|
|
122
|
+
fs = getBuiltin("node:fs");
|
|
123
|
+
path = getBuiltin("node:path");
|
|
124
|
+
} else if (typeof require === "function") {
|
|
125
|
+
fs = require("fs");
|
|
126
|
+
path = require("path");
|
|
127
|
+
}
|
|
128
|
+
if (fs && path) {
|
|
129
|
+
const configPath = path.resolve(process.cwd(), "config.ahko.json");
|
|
130
|
+
if (fs.existsSync(configPath)) {
|
|
131
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
132
|
+
activeConfig = JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function getActiveConfig() {
|
|
139
|
+
if (activeConfig === void 0) {
|
|
140
|
+
tryAutoDiscoverSync();
|
|
141
|
+
}
|
|
142
|
+
return activeConfig;
|
|
143
|
+
}
|
|
144
|
+
function getProfileConfig(profileName) {
|
|
145
|
+
const config = getActiveConfig();
|
|
146
|
+
if (!config) {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
if (profileName) {
|
|
150
|
+
return config.profiles?.[profileName];
|
|
151
|
+
}
|
|
152
|
+
return config.default;
|
|
153
|
+
}
|
|
154
|
+
|
|
69
155
|
// src/models/strategy.model.ts
|
|
70
156
|
var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
71
157
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
@@ -76,6 +162,23 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
76
162
|
return EScheduleStrategy2;
|
|
77
163
|
})(EScheduleStrategy || {});
|
|
78
164
|
|
|
165
|
+
// src/errors/circuit-breaker.error.ts
|
|
166
|
+
var AhkoCircuitBreakerOpenError = class extends AhkoError {
|
|
167
|
+
/** Time remaining in milliseconds before trial execution is allowed */
|
|
168
|
+
resetTimeoutMs;
|
|
169
|
+
/** Timestamp when the circuit tripped open */
|
|
170
|
+
trippedAt;
|
|
171
|
+
/** Total consecutive failures that caused the trip */
|
|
172
|
+
consecutiveFailures;
|
|
173
|
+
constructor(message = "Circuit breaker is open. Fast-failing task execution to protect downstream resources.", options) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.name = "AhkoCircuitBreakerOpenError";
|
|
176
|
+
this.resetTimeoutMs = options?.resetTimeoutMs;
|
|
177
|
+
this.trippedAt = options?.trippedAt;
|
|
178
|
+
this.consecutiveFailures = options?.consecutiveFailures;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
79
182
|
// src/errors/timeout.error.ts
|
|
80
183
|
var AhkoTimeoutError = class extends AhkoError {
|
|
81
184
|
/**
|
|
@@ -96,6 +199,28 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
96
199
|
}
|
|
97
200
|
};
|
|
98
201
|
|
|
202
|
+
// src/models/priority.model.ts
|
|
203
|
+
var TASK_PRIORITY_WEIGHTS = {
|
|
204
|
+
high: 10,
|
|
205
|
+
normal: 0,
|
|
206
|
+
low: -10
|
|
207
|
+
};
|
|
208
|
+
function resolvePriorityWeight(priority) {
|
|
209
|
+
if (priority === void 0) {
|
|
210
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
211
|
+
}
|
|
212
|
+
if (typeof priority === "number") {
|
|
213
|
+
return Number.isFinite(priority) ? priority : TASK_PRIORITY_WEIGHTS.normal;
|
|
214
|
+
}
|
|
215
|
+
if (priority === "high") {
|
|
216
|
+
return TASK_PRIORITY_WEIGHTS.high;
|
|
217
|
+
}
|
|
218
|
+
if (priority === "low") {
|
|
219
|
+
return TASK_PRIORITY_WEIGHTS.low;
|
|
220
|
+
}
|
|
221
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
222
|
+
}
|
|
223
|
+
|
|
99
224
|
// src/models/state.model.ts
|
|
100
225
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
101
226
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -132,6 +257,123 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
132
257
|
return Math.floor(cappedDelay);
|
|
133
258
|
}
|
|
134
259
|
|
|
260
|
+
// src/models/circuit-breaker.model.ts
|
|
261
|
+
var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
|
|
262
|
+
ECircuitState2["CLOSED"] = "closed";
|
|
263
|
+
ECircuitState2["OPEN"] = "open";
|
|
264
|
+
ECircuitState2["HALF_OPEN"] = "half_open";
|
|
265
|
+
return ECircuitState2;
|
|
266
|
+
})(ECircuitState || {});
|
|
267
|
+
|
|
268
|
+
// src/scheduler/circuit-breaker.ts
|
|
269
|
+
var CircuitBreakerCoordinator = class {
|
|
270
|
+
_state = "closed" /* CLOSED */;
|
|
271
|
+
_consecutiveFailures = 0;
|
|
272
|
+
_lastFailureTime;
|
|
273
|
+
failureThreshold;
|
|
274
|
+
resetTimeoutMs;
|
|
275
|
+
/**
|
|
276
|
+
* Initializes a new CircuitBreakerCoordinator.
|
|
277
|
+
*
|
|
278
|
+
* @param options - Configuration options for threshold and cool-down window.
|
|
279
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
280
|
+
*/
|
|
281
|
+
constructor(options) {
|
|
282
|
+
if (typeof options.failureThreshold !== "number" || Number.isNaN(options.failureThreshold) || !Number.isInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
283
|
+
throw new AhkoConfigurationError(
|
|
284
|
+
`Invalid failureThreshold "${options.failureThreshold}". failureThreshold must be an integer greater than or equal to 1.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (typeof options.resetTimeoutMs !== "number" || Number.isNaN(options.resetTimeoutMs) || !Number.isFinite(options.resetTimeoutMs) || options.resetTimeoutMs <= 0) {
|
|
288
|
+
throw new AhkoConfigurationError(
|
|
289
|
+
`Invalid resetTimeoutMs "${options.resetTimeoutMs}". resetTimeoutMs must be a positive finite number greater than 0.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
this.failureThreshold = options.failureThreshold;
|
|
293
|
+
this.resetTimeoutMs = options.resetTimeoutMs;
|
|
294
|
+
}
|
|
295
|
+
/** Current state of the circuit breaker */
|
|
296
|
+
get state() {
|
|
297
|
+
this.refreshState();
|
|
298
|
+
return this._state;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Checks whether an execution is currently allowed.
|
|
302
|
+
* If the circuit is OPEN and cool-down has not elapsed, fast-fails immediately.
|
|
303
|
+
*
|
|
304
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit is currently OPEN.
|
|
305
|
+
*/
|
|
306
|
+
checkAllowed() {
|
|
307
|
+
this.refreshState();
|
|
308
|
+
if (this._state === "open" /* OPEN */) {
|
|
309
|
+
const remainingMs = this._lastFailureTime ? Math.max(0, this.resetTimeoutMs - (Date.now() - this._lastFailureTime)) : this.resetTimeoutMs;
|
|
310
|
+
throw new AhkoCircuitBreakerOpenError(
|
|
311
|
+
`Circuit breaker is open. Fast-failing task execution. Remaining cool-down: ${remainingMs}ms.`,
|
|
312
|
+
{
|
|
313
|
+
resetTimeoutMs: remainingMs,
|
|
314
|
+
trippedAt: this._lastFailureTime,
|
|
315
|
+
consecutiveFailures: this._consecutiveFailures
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Records a successful task execution.
|
|
322
|
+
* Heals HALF_OPEN state back to CLOSED and resets consecutive failure counters.
|
|
323
|
+
*/
|
|
324
|
+
recordSuccess() {
|
|
325
|
+
this._consecutiveFailures = 0;
|
|
326
|
+
this._state = "closed" /* CLOSED */;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Records a failed task execution.
|
|
330
|
+
* Trips CLOSED to OPEN when threshold is met, or re-trips HALF_OPEN immediately.
|
|
331
|
+
*
|
|
332
|
+
* @param _error - Optional error that caused the failure.
|
|
333
|
+
*/
|
|
334
|
+
recordFailure(_error) {
|
|
335
|
+
this._consecutiveFailures++;
|
|
336
|
+
this._lastFailureTime = Date.now();
|
|
337
|
+
if (this._state === "half_open" /* HALF_OPEN */) {
|
|
338
|
+
this._state = "open" /* OPEN */;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (this._consecutiveFailures >= this.failureThreshold) {
|
|
342
|
+
this._state = "open" /* OPEN */;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Evaluates if enough time has passed to transition from OPEN to HALF_OPEN.
|
|
347
|
+
*/
|
|
348
|
+
refreshState() {
|
|
349
|
+
if (this._state === "open" /* OPEN */ && this._lastFailureTime !== void 0) {
|
|
350
|
+
const elapsed = Date.now() - this._lastFailureTime;
|
|
351
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
352
|
+
this._state = "half_open" /* HALF_OPEN */;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Resets the circuit breaker back to initial CLOSED state.
|
|
358
|
+
*/
|
|
359
|
+
reset() {
|
|
360
|
+
this._state = "closed" /* CLOSED */;
|
|
361
|
+
this._consecutiveFailures = 0;
|
|
362
|
+
this._lastFailureTime = void 0;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Returns a snapshot of circuit breaker telemetry.
|
|
366
|
+
*/
|
|
367
|
+
getStats() {
|
|
368
|
+
this.refreshState();
|
|
369
|
+
return {
|
|
370
|
+
state: this._state,
|
|
371
|
+
consecutiveFailures: this._consecutiveFailures,
|
|
372
|
+
lastFailureTime: this._lastFailureTime
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
135
377
|
// src/errors/cancellation.error.ts
|
|
136
378
|
var AhkoCancellationError = class extends AhkoError {
|
|
137
379
|
/**
|
|
@@ -150,6 +392,8 @@ var AhkoCancellationError = class extends AhkoError {
|
|
|
150
392
|
// src/scheduler/debounce-coordinator.ts
|
|
151
393
|
var DebounceCoordinator = class {
|
|
152
394
|
entries = /* @__PURE__ */ new Map();
|
|
395
|
+
/** Optional callback invoked whenever entries are settled or removed from coordinator */
|
|
396
|
+
onSettled;
|
|
153
397
|
/**
|
|
154
398
|
* Schedules a task under the debounce strategy.
|
|
155
399
|
*
|
|
@@ -236,6 +480,7 @@ var DebounceCoordinator = class {
|
|
|
236
480
|
return;
|
|
237
481
|
}
|
|
238
482
|
this.entries.delete(key);
|
|
483
|
+
this.onSettled?.();
|
|
239
484
|
if (entry.options?.signal && entry.abortListener) {
|
|
240
485
|
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
241
486
|
}
|
|
@@ -259,6 +504,7 @@ var DebounceCoordinator = class {
|
|
|
259
504
|
}
|
|
260
505
|
clearTimeout(entry.timerId);
|
|
261
506
|
this.entries.delete(key);
|
|
507
|
+
this.onSettled?.();
|
|
262
508
|
if (entry.options?.signal && entry.abortListener) {
|
|
263
509
|
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
264
510
|
}
|
|
@@ -286,6 +532,7 @@ var DebounceCoordinator = class {
|
|
|
286
532
|
entry.reject(new AhkoCancellationError("Debounced tasks cleared"));
|
|
287
533
|
}
|
|
288
534
|
this.entries.clear();
|
|
535
|
+
this.onSettled?.();
|
|
289
536
|
}
|
|
290
537
|
};
|
|
291
538
|
|
|
@@ -331,6 +578,8 @@ var IdleScheduler = class {
|
|
|
331
578
|
// src/scheduler/throttle-coordinator.ts
|
|
332
579
|
var ThrottleCoordinator = class {
|
|
333
580
|
entries = /* @__PURE__ */ new Map();
|
|
581
|
+
/** Optional callback invoked whenever entries are settled or removed from coordinator */
|
|
582
|
+
onSettled;
|
|
334
583
|
/**
|
|
335
584
|
* Schedules a task under the throttle strategy.
|
|
336
585
|
*
|
|
@@ -416,6 +665,7 @@ var ThrottleCoordinator = class {
|
|
|
416
665
|
return;
|
|
417
666
|
}
|
|
418
667
|
this.entries.delete(key);
|
|
668
|
+
this.onSettled?.();
|
|
419
669
|
}
|
|
420
670
|
/**
|
|
421
671
|
* Cancels any pending trailing throttled task for a given key.
|
|
@@ -432,6 +682,7 @@ var ThrottleCoordinator = class {
|
|
|
432
682
|
clearTimeout(entry.windowTimerId);
|
|
433
683
|
}
|
|
434
684
|
this.entries.delete(key);
|
|
685
|
+
this.onSettled?.();
|
|
435
686
|
if (entry.trailingReject) {
|
|
436
687
|
const cancelError = new AhkoCancellationError(
|
|
437
688
|
typeof reason === "string" ? reason : "Throttled task was cancelled",
|
|
@@ -459,6 +710,7 @@ var ThrottleCoordinator = class {
|
|
|
459
710
|
}
|
|
460
711
|
}
|
|
461
712
|
this.entries.clear();
|
|
713
|
+
this.onSettled?.();
|
|
462
714
|
}
|
|
463
715
|
};
|
|
464
716
|
|
|
@@ -556,6 +808,10 @@ var TaskQueue = class {
|
|
|
556
808
|
throttleCoordinator = new ThrottleCoordinator();
|
|
557
809
|
/** Lifecycle event emitter for task and scheduler events */
|
|
558
810
|
emitter = new AhkoEventEmitter();
|
|
811
|
+
/** Circuit breaker coordinator if configured */
|
|
812
|
+
circuitBreakerCoordinator;
|
|
813
|
+
/** Pause state flag */
|
|
814
|
+
_isPaused = false;
|
|
559
815
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
560
816
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
561
817
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -577,9 +833,10 @@ var TaskQueue = class {
|
|
|
577
833
|
*
|
|
578
834
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
579
835
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
836
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
580
837
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
581
838
|
*/
|
|
582
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
839
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
|
|
583
840
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
584
841
|
throw new AhkoConfigurationError(
|
|
585
842
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -592,6 +849,50 @@ var TaskQueue = class {
|
|
|
592
849
|
}
|
|
593
850
|
this.concurrency = concurrency;
|
|
594
851
|
this.minIntervalMs = minIntervalMs;
|
|
852
|
+
if (circuitBreakerOptions) {
|
|
853
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
854
|
+
}
|
|
855
|
+
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
856
|
+
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
|
|
860
|
+
*/
|
|
861
|
+
pause() {
|
|
862
|
+
this._isPaused = true;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Resumes queue execution, immediately dispatching waiting tasks up to available concurrency.
|
|
866
|
+
*/
|
|
867
|
+
resume() {
|
|
868
|
+
if (this._isPaused) {
|
|
869
|
+
this._isPaused = false;
|
|
870
|
+
this.pump();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Checks whether the task queue is currently paused.
|
|
875
|
+
*/
|
|
876
|
+
isPaused() {
|
|
877
|
+
return this._isPaused;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Inserts a task runner into the queue based on priority weight (descending).
|
|
881
|
+
* Preserves FIFO ordering among tasks with identical priority.
|
|
882
|
+
*/
|
|
883
|
+
insertIntoQueue(runner) {
|
|
884
|
+
const options = this.runnerOptions.get(runner);
|
|
885
|
+
const targetWeight = resolvePriorityWeight(options?.priority);
|
|
886
|
+
let insertIndex = this.queue.length;
|
|
887
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
888
|
+
const existingOptions = this.runnerOptions.get(this.queue[i]);
|
|
889
|
+
const existingWeight = resolvePriorityWeight(existingOptions?.priority);
|
|
890
|
+
if (existingWeight < targetWeight) {
|
|
891
|
+
insertIndex = i;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
this.queue.splice(insertIndex, 0, runner);
|
|
595
896
|
}
|
|
596
897
|
/**
|
|
597
898
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -646,6 +947,13 @@ var TaskQueue = class {
|
|
|
646
947
|
);
|
|
647
948
|
}
|
|
648
949
|
}
|
|
950
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
951
|
+
if (typeof options.totalTimeoutMs !== "number" || Number.isNaN(options.totalTimeoutMs) || !Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0) {
|
|
952
|
+
throw new AhkoConfigurationError(
|
|
953
|
+
`Invalid totalTimeoutMs "${options.totalTimeoutMs}". totalTimeoutMs must be a positive finite number greater than 0.`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
649
957
|
if (options) {
|
|
650
958
|
this.runnerOptions.set(runner, options);
|
|
651
959
|
}
|
|
@@ -653,6 +961,16 @@ var TaskQueue = class {
|
|
|
653
961
|
this.cancelledTasks++;
|
|
654
962
|
return runner.promise;
|
|
655
963
|
}
|
|
964
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
965
|
+
const budgetMs = options.totalTimeoutMs;
|
|
966
|
+
const totalTimerId = setTimeout(() => {
|
|
967
|
+
runner.timeout(budgetMs, `Task total execution deadline exceeded after ${budgetMs}ms`);
|
|
968
|
+
}, budgetMs);
|
|
969
|
+
runner.promise.finally(() => {
|
|
970
|
+
clearTimeout(totalTimerId);
|
|
971
|
+
}).catch(() => {
|
|
972
|
+
});
|
|
973
|
+
}
|
|
656
974
|
if (strategy === "delay" /* DELAY */) {
|
|
657
975
|
const delayMs = options?.delay ?? 0;
|
|
658
976
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -676,15 +994,23 @@ var TaskQueue = class {
|
|
|
676
994
|
const index = this.queue.indexOf(runner);
|
|
677
995
|
if (index !== -1) {
|
|
678
996
|
this.queue.splice(index, 1);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
997
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
998
|
+
this.timedOutTasks++;
|
|
999
|
+
this.emitter.emit("task:timeout", {
|
|
1000
|
+
taskId: runner.taskId,
|
|
1001
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1002
|
+
});
|
|
1003
|
+
} else {
|
|
1004
|
+
this.cancelledTasks++;
|
|
1005
|
+
this.emitter.emit("task:cancel", {
|
|
1006
|
+
taskId: runner.taskId,
|
|
1007
|
+
reason: "Task cancelled while queued"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
684
1010
|
this.checkIdle();
|
|
685
1011
|
}
|
|
686
1012
|
};
|
|
687
|
-
this.
|
|
1013
|
+
this.insertIntoQueue(runner);
|
|
688
1014
|
this.pump();
|
|
689
1015
|
return runner.promise;
|
|
690
1016
|
}
|
|
@@ -697,22 +1023,30 @@ var TaskQueue = class {
|
|
|
697
1023
|
runner,
|
|
698
1024
|
timerId: setTimeout(() => {
|
|
699
1025
|
this.delayedEntries.delete(delayedEntry);
|
|
700
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1026
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
701
1027
|
return;
|
|
702
1028
|
}
|
|
703
1029
|
runner.onCancel = () => {
|
|
704
1030
|
const index = this.queue.indexOf(runner);
|
|
705
1031
|
if (index !== -1) {
|
|
706
1032
|
this.queue.splice(index, 1);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1033
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1034
|
+
this.timedOutTasks++;
|
|
1035
|
+
this.emitter.emit("task:timeout", {
|
|
1036
|
+
taskId: runner.taskId,
|
|
1037
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1038
|
+
});
|
|
1039
|
+
} else {
|
|
1040
|
+
this.cancelledTasks++;
|
|
1041
|
+
this.emitter.emit("task:cancel", {
|
|
1042
|
+
taskId: runner.taskId,
|
|
1043
|
+
reason: "Task cancelled while queued"
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
712
1046
|
this.checkIdle();
|
|
713
1047
|
}
|
|
714
1048
|
};
|
|
715
|
-
this.
|
|
1049
|
+
this.insertIntoQueue(runner);
|
|
716
1050
|
this.pump();
|
|
717
1051
|
}, delayMs)
|
|
718
1052
|
};
|
|
@@ -721,11 +1055,19 @@ var TaskQueue = class {
|
|
|
721
1055
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
722
1056
|
clearTimeout(delayedEntry.timerId);
|
|
723
1057
|
this.delayedEntries.delete(delayedEntry);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1058
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1059
|
+
this.timedOutTasks++;
|
|
1060
|
+
this.emitter.emit("task:timeout", {
|
|
1061
|
+
taskId: runner.taskId,
|
|
1062
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1063
|
+
});
|
|
1064
|
+
} else {
|
|
1065
|
+
this.cancelledTasks++;
|
|
1066
|
+
this.emitter.emit("task:cancel", {
|
|
1067
|
+
taskId: runner.taskId,
|
|
1068
|
+
reason: "Task cancelled while waiting in delay"
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
729
1071
|
this.checkIdle();
|
|
730
1072
|
}
|
|
731
1073
|
};
|
|
@@ -738,22 +1080,30 @@ var TaskQueue = class {
|
|
|
738
1080
|
let idleEntry;
|
|
739
1081
|
const handle = IdleScheduler.schedule(() => {
|
|
740
1082
|
this.idleEntries.delete(idleEntry);
|
|
741
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1083
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
742
1084
|
return;
|
|
743
1085
|
}
|
|
744
1086
|
runner.onCancel = () => {
|
|
745
1087
|
const index = this.queue.indexOf(runner);
|
|
746
1088
|
if (index !== -1) {
|
|
747
1089
|
this.queue.splice(index, 1);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1090
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1091
|
+
this.timedOutTasks++;
|
|
1092
|
+
this.emitter.emit("task:timeout", {
|
|
1093
|
+
taskId: runner.taskId,
|
|
1094
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1095
|
+
});
|
|
1096
|
+
} else {
|
|
1097
|
+
this.cancelledTasks++;
|
|
1098
|
+
this.emitter.emit("task:cancel", {
|
|
1099
|
+
taskId: runner.taskId,
|
|
1100
|
+
reason: "Task cancelled while queued"
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
753
1103
|
this.checkIdle();
|
|
754
1104
|
}
|
|
755
1105
|
};
|
|
756
|
-
this.
|
|
1106
|
+
this.insertIntoQueue(runner);
|
|
757
1107
|
this.pump();
|
|
758
1108
|
}, idleTimeout);
|
|
759
1109
|
idleEntry = { runner, handle };
|
|
@@ -762,21 +1112,30 @@ var TaskQueue = class {
|
|
|
762
1112
|
if (this.idleEntries.has(idleEntry)) {
|
|
763
1113
|
handle.cancel();
|
|
764
1114
|
this.idleEntries.delete(idleEntry);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1115
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1116
|
+
this.timedOutTasks++;
|
|
1117
|
+
this.emitter.emit("task:timeout", {
|
|
1118
|
+
taskId: runner.taskId,
|
|
1119
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1120
|
+
});
|
|
1121
|
+
} else {
|
|
1122
|
+
this.cancelledTasks++;
|
|
1123
|
+
this.emitter.emit("task:cancel", {
|
|
1124
|
+
taskId: runner.taskId,
|
|
1125
|
+
reason: "Task cancelled while waiting for idle"
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
770
1128
|
this.checkIdle();
|
|
771
1129
|
}
|
|
772
1130
|
};
|
|
773
1131
|
}
|
|
774
1132
|
/**
|
|
775
1133
|
* Pumps the queue by picking pending tasks and executing them
|
|
776
|
-
* as long as concurrency capacity is available
|
|
1134
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1135
|
+
* and queue is not paused.
|
|
777
1136
|
*/
|
|
778
1137
|
pump() {
|
|
779
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
1138
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
780
1139
|
return;
|
|
781
1140
|
}
|
|
782
1141
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -793,7 +1152,7 @@ var TaskQueue = class {
|
|
|
793
1152
|
return;
|
|
794
1153
|
}
|
|
795
1154
|
}
|
|
796
|
-
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
1155
|
+
while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
797
1156
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
798
1157
|
const now = Date.now();
|
|
799
1158
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -812,9 +1171,25 @@ var TaskQueue = class {
|
|
|
812
1171
|
if (!runner) {
|
|
813
1172
|
break;
|
|
814
1173
|
}
|
|
815
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1174
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
816
1175
|
continue;
|
|
817
1176
|
}
|
|
1177
|
+
if (this.circuitBreakerCoordinator) {
|
|
1178
|
+
try {
|
|
1179
|
+
this.circuitBreakerCoordinator.checkAllowed();
|
|
1180
|
+
} catch (cbError) {
|
|
1181
|
+
this.failedTasks++;
|
|
1182
|
+
this.runnerOptions.delete(runner);
|
|
1183
|
+
this.emitter.emit("task:fail", {
|
|
1184
|
+
taskId: runner.taskId,
|
|
1185
|
+
attempt: runner.attempt,
|
|
1186
|
+
error: cbError,
|
|
1187
|
+
willRetry: false
|
|
1188
|
+
});
|
|
1189
|
+
runner.reject(cbError);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
818
1193
|
this.activeRunners.add(runner);
|
|
819
1194
|
this.lastTaskStartTime = Date.now();
|
|
820
1195
|
void this.executeRunner(runner);
|
|
@@ -843,6 +1218,7 @@ var TaskQueue = class {
|
|
|
843
1218
|
});
|
|
844
1219
|
try {
|
|
845
1220
|
const result = await runner.run();
|
|
1221
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
846
1222
|
this.completedTasks++;
|
|
847
1223
|
this.activeRunners.delete(runner);
|
|
848
1224
|
this.runnerOptions.delete(runner);
|
|
@@ -878,11 +1254,14 @@ var TaskQueue = class {
|
|
|
878
1254
|
this.scheduleRetry(runner, options);
|
|
879
1255
|
return;
|
|
880
1256
|
}
|
|
1257
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1258
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1259
|
+
}
|
|
881
1260
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
882
1261
|
this.timedOutTasks++;
|
|
883
1262
|
this.emitter.emit("task:timeout", {
|
|
884
1263
|
taskId: runner.taskId,
|
|
885
|
-
timeoutMs: runner.timeoutMs
|
|
1264
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
886
1265
|
});
|
|
887
1266
|
} else {
|
|
888
1267
|
this.failedTasks++;
|
|
@@ -912,15 +1291,23 @@ var TaskQueue = class {
|
|
|
912
1291
|
const index = this.queue.indexOf(runner);
|
|
913
1292
|
if (index !== -1) {
|
|
914
1293
|
this.queue.splice(index, 1);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1294
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1295
|
+
this.timedOutTasks++;
|
|
1296
|
+
this.emitter.emit("task:timeout", {
|
|
1297
|
+
taskId: runner.taskId,
|
|
1298
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1299
|
+
});
|
|
1300
|
+
} else {
|
|
1301
|
+
this.cancelledTasks++;
|
|
1302
|
+
this.emitter.emit("task:cancel", {
|
|
1303
|
+
taskId: runner.taskId,
|
|
1304
|
+
reason: "Task cancelled while queued"
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
920
1307
|
this.checkIdle();
|
|
921
1308
|
}
|
|
922
1309
|
};
|
|
923
|
-
this.
|
|
1310
|
+
this.insertIntoQueue(runner);
|
|
924
1311
|
this.pump();
|
|
925
1312
|
return;
|
|
926
1313
|
}
|
|
@@ -928,22 +1315,30 @@ var TaskQueue = class {
|
|
|
928
1315
|
runner,
|
|
929
1316
|
timerId: setTimeout(() => {
|
|
930
1317
|
this.retryEntries.delete(retryEntry);
|
|
931
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1318
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
932
1319
|
return;
|
|
933
1320
|
}
|
|
934
1321
|
runner.onCancel = () => {
|
|
935
1322
|
const index = this.queue.indexOf(runner);
|
|
936
1323
|
if (index !== -1) {
|
|
937
1324
|
this.queue.splice(index, 1);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1325
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1326
|
+
this.timedOutTasks++;
|
|
1327
|
+
this.emitter.emit("task:timeout", {
|
|
1328
|
+
taskId: runner.taskId,
|
|
1329
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1330
|
+
});
|
|
1331
|
+
} else {
|
|
1332
|
+
this.cancelledTasks++;
|
|
1333
|
+
this.emitter.emit("task:cancel", {
|
|
1334
|
+
taskId: runner.taskId,
|
|
1335
|
+
reason: "Task cancelled while queued"
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
943
1338
|
this.checkIdle();
|
|
944
1339
|
}
|
|
945
1340
|
};
|
|
946
|
-
this.
|
|
1341
|
+
this.insertIntoQueue(runner);
|
|
947
1342
|
this.pump();
|
|
948
1343
|
}, backoffDelay)
|
|
949
1344
|
};
|
|
@@ -952,11 +1347,19 @@ var TaskQueue = class {
|
|
|
952
1347
|
if (this.retryEntries.has(retryEntry)) {
|
|
953
1348
|
clearTimeout(retryEntry.timerId);
|
|
954
1349
|
this.retryEntries.delete(retryEntry);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1350
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1351
|
+
this.timedOutTasks++;
|
|
1352
|
+
this.emitter.emit("task:timeout", {
|
|
1353
|
+
taskId: runner.taskId,
|
|
1354
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1355
|
+
});
|
|
1356
|
+
} else {
|
|
1357
|
+
this.cancelledTasks++;
|
|
1358
|
+
this.emitter.emit("task:cancel", {
|
|
1359
|
+
taskId: runner.taskId,
|
|
1360
|
+
reason: "Task cancelled during retry backoff"
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
960
1363
|
this.checkIdle();
|
|
961
1364
|
}
|
|
962
1365
|
};
|
|
@@ -1003,7 +1406,7 @@ var TaskQueue = class {
|
|
|
1003
1406
|
clear() {
|
|
1004
1407
|
while (this.queue.length > 0) {
|
|
1005
1408
|
const runner = this.queue.shift();
|
|
1006
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1409
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
1007
1410
|
runner.cancel("Scheduler cleared");
|
|
1008
1411
|
this.cancelledTasks++;
|
|
1009
1412
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1053,7 +1456,9 @@ var TaskQueue = class {
|
|
|
1053
1456
|
timedOutTasks: this.timedOutTasks,
|
|
1054
1457
|
retriedTasks: this.retriedTasks,
|
|
1055
1458
|
totalDispatched: this.totalDispatched,
|
|
1056
|
-
capacity: this.concurrency
|
|
1459
|
+
capacity: this.concurrency,
|
|
1460
|
+
isPaused: this._isPaused,
|
|
1461
|
+
circuitState: this.circuitBreakerCoordinator?.state
|
|
1057
1462
|
});
|
|
1058
1463
|
}
|
|
1059
1464
|
};
|
|
@@ -1124,6 +1529,8 @@ var TaskRunner = class {
|
|
|
1124
1529
|
}
|
|
1125
1530
|
}
|
|
1126
1531
|
}
|
|
1532
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1533
|
+
totalTimedOut = false;
|
|
1127
1534
|
/**
|
|
1128
1535
|
* Gets the current lifecycle state of the task.
|
|
1129
1536
|
*/
|
|
@@ -1156,7 +1563,7 @@ var TaskRunner = class {
|
|
|
1156
1563
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1157
1564
|
*/
|
|
1158
1565
|
async canRetry(error, retryOptions) {
|
|
1159
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1566
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1160
1567
|
return false;
|
|
1161
1568
|
}
|
|
1162
1569
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1197,15 +1604,16 @@ var TaskRunner = class {
|
|
|
1197
1604
|
let abortListener;
|
|
1198
1605
|
const abortPromise = new Promise((_, reject) => {
|
|
1199
1606
|
abortListener = () => {
|
|
1200
|
-
|
|
1607
|
+
const reason = this.abortController.signal.reason;
|
|
1608
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || reason instanceof AhkoTimeoutError) {
|
|
1609
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1201
1610
|
reject(
|
|
1202
|
-
new AhkoTimeoutError(
|
|
1611
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1203
1612
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1204
1613
|
{ timeoutMs: this.timeoutMs }
|
|
1205
1614
|
)
|
|
1206
1615
|
);
|
|
1207
1616
|
} else {
|
|
1208
|
-
const reason = this.abortController.signal.reason;
|
|
1209
1617
|
reject(
|
|
1210
1618
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1211
1619
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1240,6 +1648,10 @@ var TaskRunner = class {
|
|
|
1240
1648
|
}
|
|
1241
1649
|
taskExecutionPromise.catch(() => {
|
|
1242
1650
|
});
|
|
1651
|
+
abortPromise.catch(() => {
|
|
1652
|
+
});
|
|
1653
|
+
timeoutPromise?.catch(() => {
|
|
1654
|
+
});
|
|
1243
1655
|
const racePromises = [
|
|
1244
1656
|
taskExecutionPromise,
|
|
1245
1657
|
abortPromise
|
|
@@ -1331,6 +1743,31 @@ var TaskRunner = class {
|
|
|
1331
1743
|
this.onCancel?.(this);
|
|
1332
1744
|
}
|
|
1333
1745
|
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Times out the task, aborting pending or running execution with AhkoTimeoutError.
|
|
1748
|
+
*
|
|
1749
|
+
* @param timeoutMs - Timeout duration in milliseconds.
|
|
1750
|
+
* @param message - Optional custom timeout message.
|
|
1751
|
+
*/
|
|
1752
|
+
timeout(timeoutMs, message) {
|
|
1753
|
+
if (this._state === "completed" /* COMPLETED */ || this._state === "failed" /* FAILED */ || this._state === "cancelled" /* CANCELLED */ || this._state === "timed_out" /* TIMED_OUT */) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const wasPending = this._state === "pending" /* PENDING */;
|
|
1757
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1758
|
+
this.totalTimedOut = true;
|
|
1759
|
+
this.clearTimeoutTimer();
|
|
1760
|
+
const timeoutError = new AhkoTimeoutError(
|
|
1761
|
+
message ?? `Task execution timed out after ${timeoutMs}ms`,
|
|
1762
|
+
{ timeoutMs }
|
|
1763
|
+
);
|
|
1764
|
+
this.abortController.abort(timeoutError);
|
|
1765
|
+
this.cleanup();
|
|
1766
|
+
if (wasPending) {
|
|
1767
|
+
this.rejectPromise(timeoutError);
|
|
1768
|
+
this.onCancel?.(this);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1334
1771
|
/**
|
|
1335
1772
|
* Handles external AbortSignal trigger.
|
|
1336
1773
|
*/
|
|
@@ -1349,9 +1786,55 @@ var TaskRunner = class {
|
|
|
1349
1786
|
};
|
|
1350
1787
|
|
|
1351
1788
|
// src/ahko.ts
|
|
1352
|
-
var Ahko = class {
|
|
1789
|
+
var Ahko = class _Ahko {
|
|
1353
1790
|
/** Internal queue and concurrency manager */
|
|
1354
1791
|
queue;
|
|
1792
|
+
/** Default schedule options inherited from profile if configured */
|
|
1793
|
+
defaultScheduleOptions;
|
|
1794
|
+
/**
|
|
1795
|
+
* Programmatically loads a declarative configuration into memory.
|
|
1796
|
+
* Works universally across Node.js, browsers, and edge runtimes.
|
|
1797
|
+
*
|
|
1798
|
+
* @param config - File configuration object containing default and named profiles.
|
|
1799
|
+
*/
|
|
1800
|
+
static loadConfig(config) {
|
|
1801
|
+
loadConfig(config);
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Asynchronously loads a configuration file from disk (Node.js).
|
|
1805
|
+
*
|
|
1806
|
+
* @param filePath - Path to configuration file (default: "config.ahko.json").
|
|
1807
|
+
*/
|
|
1808
|
+
static async loadConfigFile(filePath) {
|
|
1809
|
+
return loadConfigFile(filePath);
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Resets the active declarative configuration.
|
|
1813
|
+
*/
|
|
1814
|
+
static resetConfig() {
|
|
1815
|
+
resetConfig();
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Retrieves the currently active declarative configuration.
|
|
1819
|
+
*/
|
|
1820
|
+
static getActiveConfig() {
|
|
1821
|
+
return getActiveConfig();
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Instantiates an Ahko scheduler initialized with settings from a declarative profile.
|
|
1825
|
+
*
|
|
1826
|
+
* @param profileName - Optional name of the profile (e.g. "api", "background").
|
|
1827
|
+
* @param overrides - Optional scheduler options overriding profile values.
|
|
1828
|
+
* @returns A new configured Ahko instance.
|
|
1829
|
+
*/
|
|
1830
|
+
static fromProfile(profileName, overrides) {
|
|
1831
|
+
const profile = getProfileConfig(profileName);
|
|
1832
|
+
return new _Ahko({
|
|
1833
|
+
...profile,
|
|
1834
|
+
...overrides,
|
|
1835
|
+
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1355
1838
|
/**
|
|
1356
1839
|
* Initializes a new Ahko scheduler instance.
|
|
1357
1840
|
*
|
|
@@ -1364,79 +1847,152 @@ var Ahko = class {
|
|
|
1364
1847
|
* ```
|
|
1365
1848
|
*/
|
|
1366
1849
|
constructor(options) {
|
|
1367
|
-
|
|
1850
|
+
const profile = options?.profile ? getProfileConfig(options.profile) : getProfileConfig();
|
|
1851
|
+
const mergedOptions = {
|
|
1852
|
+
...profile,
|
|
1853
|
+
...options,
|
|
1854
|
+
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker
|
|
1855
|
+
};
|
|
1856
|
+
if (profile) {
|
|
1857
|
+
this.defaultScheduleOptions = {
|
|
1858
|
+
priority: profile.priority,
|
|
1859
|
+
retry: profile.retry,
|
|
1860
|
+
timeoutMs: profile.timeoutMs,
|
|
1861
|
+
totalTimeoutMs: profile.totalTimeoutMs
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
this.queue = new TaskQueue(
|
|
1865
|
+
mergedOptions.concurrency,
|
|
1866
|
+
mergedOptions.minIntervalMs,
|
|
1867
|
+
mergedOptions.circuitBreaker
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
|
|
1872
|
+
*/
|
|
1873
|
+
pause() {
|
|
1874
|
+
this.queue.pause();
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Resumes scheduler dispatch, immediately executing waiting tasks up to available concurrency.
|
|
1878
|
+
*/
|
|
1879
|
+
resume() {
|
|
1880
|
+
this.queue.resume();
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Checks whether the scheduler is currently paused.
|
|
1884
|
+
*/
|
|
1885
|
+
isPaused() {
|
|
1886
|
+
return this.queue.isPaused();
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Current circuit breaker state if circuit breaker protection is configured.
|
|
1890
|
+
*/
|
|
1891
|
+
get circuitState() {
|
|
1892
|
+
return this.queue.circuitBreakerCoordinator?.state;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Access to the underlying circuit breaker coordinator instance if configured.
|
|
1896
|
+
*/
|
|
1897
|
+
get circuitBreaker() {
|
|
1898
|
+
return this.queue.circuitBreakerCoordinator;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Wraps an async function so every execution is automatically routed through this Ahko scheduler.
|
|
1902
|
+
*
|
|
1903
|
+
* @template TArgs - Parameter types of the wrapped function.
|
|
1904
|
+
* @template TReturn - Return type of the wrapped function.
|
|
1905
|
+
* @param fn - The function to wrap.
|
|
1906
|
+
* @param options - Optional scheduling options applied to every wrapped call.
|
|
1907
|
+
* @returns A wrapped function returning a Promise.
|
|
1908
|
+
*
|
|
1909
|
+
* @example
|
|
1910
|
+
* ```typescript
|
|
1911
|
+
* const fetchUser = ahko.wrap(async (id: string) => api.getUser(id), { priority: "high" });
|
|
1912
|
+
* const user = await fetchUser("usr_123");
|
|
1913
|
+
* ```
|
|
1914
|
+
*/
|
|
1915
|
+
wrap(fn, options) {
|
|
1916
|
+
if (typeof fn !== "function") {
|
|
1917
|
+
throw new AhkoConfigurationError("Target to wrap must be a valid function.");
|
|
1918
|
+
}
|
|
1919
|
+
return (...args) => {
|
|
1920
|
+
return this.schedule(() => fn(...args), options);
|
|
1921
|
+
};
|
|
1368
1922
|
}
|
|
1369
1923
|
/**
|
|
1370
1924
|
* Schedules a task for execution with full return type inference.
|
|
1371
1925
|
*
|
|
1372
1926
|
* @template T - Inferred return type of the task.
|
|
1373
1927
|
* @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
|
|
1374
|
-
* @param options - Task-specific scheduling options such as strategy, delay, and cancellation signal.
|
|
1928
|
+
* @param options - Task-specific scheduling options such as strategy, priority, delay, and cancellation signal.
|
|
1375
1929
|
* @returns A promise that resolves with the task's return value.
|
|
1376
1930
|
*
|
|
1377
1931
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1378
1932
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1379
|
-
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
1933
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs or totalTimeoutMs.
|
|
1934
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit breaker is OPEN and rejects the execution.
|
|
1380
1935
|
*
|
|
1381
1936
|
* @example
|
|
1382
1937
|
* ```typescript
|
|
1383
1938
|
* // Immediate execution (subject to concurrency)
|
|
1384
1939
|
* const count = await ahko.schedule(async () => 42);
|
|
1385
1940
|
*
|
|
1386
|
-
* //
|
|
1387
|
-
* await ahko.schedule(
|
|
1388
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1389
|
-
* { strategy: "delay", delay: 1000 }
|
|
1390
|
-
* );
|
|
1941
|
+
* // High priority task
|
|
1942
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1391
1943
|
* ```
|
|
1392
1944
|
*/
|
|
1393
1945
|
schedule(task, options) {
|
|
1394
1946
|
if (typeof task !== "function") {
|
|
1395
1947
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1396
1948
|
}
|
|
1397
|
-
const
|
|
1949
|
+
const mergedOptions = {
|
|
1950
|
+
...this.defaultScheduleOptions,
|
|
1951
|
+
...options
|
|
1952
|
+
};
|
|
1953
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1398
1954
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1399
|
-
if (!
|
|
1955
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1400
1956
|
throw new AhkoConfigurationError(
|
|
1401
1957
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1402
1958
|
);
|
|
1403
1959
|
}
|
|
1404
|
-
const waitMs =
|
|
1960
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1405
1961
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1406
1962
|
throw new AhkoConfigurationError(
|
|
1407
1963
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1408
1964
|
);
|
|
1409
1965
|
}
|
|
1410
1966
|
return this.queue.debounceCoordinator.schedule(
|
|
1411
|
-
|
|
1967
|
+
mergedOptions.key,
|
|
1412
1968
|
task,
|
|
1413
1969
|
waitMs,
|
|
1414
|
-
|
|
1970
|
+
mergedOptions,
|
|
1415
1971
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1416
1972
|
);
|
|
1417
1973
|
}
|
|
1418
1974
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1419
|
-
if (!
|
|
1975
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1420
1976
|
throw new AhkoConfigurationError(
|
|
1421
1977
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1422
1978
|
);
|
|
1423
1979
|
}
|
|
1424
|
-
const waitMs =
|
|
1980
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1425
1981
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1426
1982
|
throw new AhkoConfigurationError(
|
|
1427
1983
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1428
1984
|
);
|
|
1429
1985
|
}
|
|
1430
1986
|
return this.queue.throttleCoordinator.schedule(
|
|
1431
|
-
|
|
1987
|
+
mergedOptions.key,
|
|
1432
1988
|
task,
|
|
1433
1989
|
waitMs,
|
|
1434
|
-
|
|
1990
|
+
mergedOptions,
|
|
1435
1991
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1436
1992
|
);
|
|
1437
1993
|
}
|
|
1438
|
-
const runner = new TaskRunner(task,
|
|
1439
|
-
return this.queue.enqueue(runner,
|
|
1994
|
+
const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
|
|
1995
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1440
1996
|
}
|
|
1441
1997
|
/**
|
|
1442
1998
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1499,12 +2055,12 @@ var Ahko = class {
|
|
|
1499
2055
|
/**
|
|
1500
2056
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1501
2057
|
*
|
|
1502
|
-
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled,
|
|
2058
|
+
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, timed out tasks, pause status, and circuit state.
|
|
1503
2059
|
*
|
|
1504
2060
|
* @example
|
|
1505
2061
|
* ```typescript
|
|
1506
2062
|
* const stats = ahko.stats();
|
|
1507
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2063
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1508
2064
|
* ```
|
|
1509
2065
|
*/
|
|
1510
2066
|
stats() {
|
|
@@ -1590,7 +2146,7 @@ var Ahko = class {
|
|
|
1590
2146
|
};
|
|
1591
2147
|
|
|
1592
2148
|
// src/version.ts
|
|
1593
|
-
var VERSION = "
|
|
2149
|
+
var VERSION = "1.1.0";
|
|
1594
2150
|
|
|
1595
2151
|
// src/errors/queue.error.ts
|
|
1596
2152
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1665,16 +2221,26 @@ function combineSignals(signals) {
|
|
|
1665
2221
|
0 && (module.exports = {
|
|
1666
2222
|
Ahko,
|
|
1667
2223
|
AhkoCancellationError,
|
|
2224
|
+
AhkoCircuitBreakerOpenError,
|
|
1668
2225
|
AhkoConfigurationError,
|
|
1669
2226
|
AhkoError,
|
|
1670
2227
|
AhkoQueueError,
|
|
1671
2228
|
AhkoTimeoutError,
|
|
2229
|
+
CircuitBreakerCoordinator,
|
|
1672
2230
|
DEFAULT_BASE_DELAY,
|
|
1673
2231
|
DEFAULT_MAX_DELAY,
|
|
2232
|
+
ECircuitState,
|
|
1674
2233
|
EScheduleStrategy,
|
|
1675
2234
|
ETaskState,
|
|
2235
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1676
2236
|
VERSION,
|
|
1677
2237
|
calculateBackoff,
|
|
1678
|
-
combineSignals
|
|
2238
|
+
combineSignals,
|
|
2239
|
+
getActiveConfig,
|
|
2240
|
+
getProfileConfig,
|
|
2241
|
+
loadConfig,
|
|
2242
|
+
loadConfigFile,
|
|
2243
|
+
resetConfig,
|
|
2244
|
+
resolvePriorityWeight
|
|
1679
2245
|
});
|
|
1680
2246
|
//# sourceMappingURL=index.cjs.map
|