@mrjacket/ahko 1.0.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 +34 -0
- package/README.md +140 -6
- 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 +639 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +625 -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/task-queue.d.ts +29 -4
- package/dist/scheduler/task-runner.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -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
|
/**
|
|
@@ -528,6 +757,10 @@ var TaskQueue = class {
|
|
|
528
757
|
throttleCoordinator = new ThrottleCoordinator();
|
|
529
758
|
/** Lifecycle event emitter for task and scheduler events */
|
|
530
759
|
emitter = new AhkoEventEmitter();
|
|
760
|
+
/** Circuit breaker coordinator if configured */
|
|
761
|
+
circuitBreakerCoordinator;
|
|
762
|
+
/** Pause state flag */
|
|
763
|
+
_isPaused = false;
|
|
531
764
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
532
765
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
533
766
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -549,9 +782,10 @@ var TaskQueue = class {
|
|
|
549
782
|
*
|
|
550
783
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
551
784
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
785
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
552
786
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
553
787
|
*/
|
|
554
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
788
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
|
|
555
789
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
556
790
|
throw new AhkoConfigurationError(
|
|
557
791
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -564,9 +798,51 @@ var TaskQueue = class {
|
|
|
564
798
|
}
|
|
565
799
|
this.concurrency = concurrency;
|
|
566
800
|
this.minIntervalMs = minIntervalMs;
|
|
801
|
+
if (circuitBreakerOptions) {
|
|
802
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
803
|
+
}
|
|
567
804
|
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
568
805
|
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
569
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);
|
|
845
|
+
}
|
|
570
846
|
/**
|
|
571
847
|
* Enqueues a task runner according to the specified schedule options.
|
|
572
848
|
*
|
|
@@ -620,6 +896,13 @@ var TaskQueue = class {
|
|
|
620
896
|
);
|
|
621
897
|
}
|
|
622
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
|
+
}
|
|
623
906
|
if (options) {
|
|
624
907
|
this.runnerOptions.set(runner, options);
|
|
625
908
|
}
|
|
@@ -627,6 +910,16 @@ var TaskQueue = class {
|
|
|
627
910
|
this.cancelledTasks++;
|
|
628
911
|
return runner.promise;
|
|
629
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
|
+
}
|
|
630
923
|
if (strategy === "delay" /* DELAY */) {
|
|
631
924
|
const delayMs = options?.delay ?? 0;
|
|
632
925
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -650,15 +943,23 @@ var TaskQueue = class {
|
|
|
650
943
|
const index = this.queue.indexOf(runner);
|
|
651
944
|
if (index !== -1) {
|
|
652
945
|
this.queue.splice(index, 1);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
959
|
this.checkIdle();
|
|
659
960
|
}
|
|
660
961
|
};
|
|
661
|
-
this.
|
|
962
|
+
this.insertIntoQueue(runner);
|
|
662
963
|
this.pump();
|
|
663
964
|
return runner.promise;
|
|
664
965
|
}
|
|
@@ -671,22 +972,30 @@ var TaskQueue = class {
|
|
|
671
972
|
runner,
|
|
672
973
|
timerId: setTimeout(() => {
|
|
673
974
|
this.delayedEntries.delete(delayedEntry);
|
|
674
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
975
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
675
976
|
return;
|
|
676
977
|
}
|
|
677
978
|
runner.onCancel = () => {
|
|
678
979
|
const index = this.queue.indexOf(runner);
|
|
679
980
|
if (index !== -1) {
|
|
680
981
|
this.queue.splice(index, 1);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
}
|
|
686
995
|
this.checkIdle();
|
|
687
996
|
}
|
|
688
997
|
};
|
|
689
|
-
this.
|
|
998
|
+
this.insertIntoQueue(runner);
|
|
690
999
|
this.pump();
|
|
691
1000
|
}, delayMs)
|
|
692
1001
|
};
|
|
@@ -695,11 +1004,19 @@ var TaskQueue = class {
|
|
|
695
1004
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
696
1005
|
clearTimeout(delayedEntry.timerId);
|
|
697
1006
|
this.delayedEntries.delete(delayedEntry);
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
}
|
|
703
1020
|
this.checkIdle();
|
|
704
1021
|
}
|
|
705
1022
|
};
|
|
@@ -712,22 +1029,30 @@ var TaskQueue = class {
|
|
|
712
1029
|
let idleEntry;
|
|
713
1030
|
const handle = IdleScheduler.schedule(() => {
|
|
714
1031
|
this.idleEntries.delete(idleEntry);
|
|
715
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1032
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
716
1033
|
return;
|
|
717
1034
|
}
|
|
718
1035
|
runner.onCancel = () => {
|
|
719
1036
|
const index = this.queue.indexOf(runner);
|
|
720
1037
|
if (index !== -1) {
|
|
721
1038
|
this.queue.splice(index, 1);
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
}
|
|
727
1052
|
this.checkIdle();
|
|
728
1053
|
}
|
|
729
1054
|
};
|
|
730
|
-
this.
|
|
1055
|
+
this.insertIntoQueue(runner);
|
|
731
1056
|
this.pump();
|
|
732
1057
|
}, idleTimeout);
|
|
733
1058
|
idleEntry = { runner, handle };
|
|
@@ -736,21 +1061,30 @@ var TaskQueue = class {
|
|
|
736
1061
|
if (this.idleEntries.has(idleEntry)) {
|
|
737
1062
|
handle.cancel();
|
|
738
1063
|
this.idleEntries.delete(idleEntry);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
+
}
|
|
744
1077
|
this.checkIdle();
|
|
745
1078
|
}
|
|
746
1079
|
};
|
|
747
1080
|
}
|
|
748
1081
|
/**
|
|
749
1082
|
* Pumps the queue by picking pending tasks and executing them
|
|
750
|
-
* as long as concurrency capacity is available
|
|
1083
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1084
|
+
* and queue is not paused.
|
|
751
1085
|
*/
|
|
752
1086
|
pump() {
|
|
753
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
1087
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
754
1088
|
return;
|
|
755
1089
|
}
|
|
756
1090
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -767,7 +1101,7 @@ var TaskQueue = class {
|
|
|
767
1101
|
return;
|
|
768
1102
|
}
|
|
769
1103
|
}
|
|
770
|
-
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
1104
|
+
while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
771
1105
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
772
1106
|
const now = Date.now();
|
|
773
1107
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -786,9 +1120,25 @@ var TaskQueue = class {
|
|
|
786
1120
|
if (!runner) {
|
|
787
1121
|
break;
|
|
788
1122
|
}
|
|
789
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1123
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
790
1124
|
continue;
|
|
791
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
|
+
}
|
|
792
1142
|
this.activeRunners.add(runner);
|
|
793
1143
|
this.lastTaskStartTime = Date.now();
|
|
794
1144
|
void this.executeRunner(runner);
|
|
@@ -817,6 +1167,7 @@ var TaskQueue = class {
|
|
|
817
1167
|
});
|
|
818
1168
|
try {
|
|
819
1169
|
const result = await runner.run();
|
|
1170
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
820
1171
|
this.completedTasks++;
|
|
821
1172
|
this.activeRunners.delete(runner);
|
|
822
1173
|
this.runnerOptions.delete(runner);
|
|
@@ -852,11 +1203,14 @@ var TaskQueue = class {
|
|
|
852
1203
|
this.scheduleRetry(runner, options);
|
|
853
1204
|
return;
|
|
854
1205
|
}
|
|
1206
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1207
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1208
|
+
}
|
|
855
1209
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
856
1210
|
this.timedOutTasks++;
|
|
857
1211
|
this.emitter.emit("task:timeout", {
|
|
858
1212
|
taskId: runner.taskId,
|
|
859
|
-
timeoutMs: runner.timeoutMs
|
|
1213
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
860
1214
|
});
|
|
861
1215
|
} else {
|
|
862
1216
|
this.failedTasks++;
|
|
@@ -886,15 +1240,23 @@ var TaskQueue = class {
|
|
|
886
1240
|
const index = this.queue.indexOf(runner);
|
|
887
1241
|
if (index !== -1) {
|
|
888
1242
|
this.queue.splice(index, 1);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
}
|
|
894
1256
|
this.checkIdle();
|
|
895
1257
|
}
|
|
896
1258
|
};
|
|
897
|
-
this.
|
|
1259
|
+
this.insertIntoQueue(runner);
|
|
898
1260
|
this.pump();
|
|
899
1261
|
return;
|
|
900
1262
|
}
|
|
@@ -902,22 +1264,30 @@ var TaskQueue = class {
|
|
|
902
1264
|
runner,
|
|
903
1265
|
timerId: setTimeout(() => {
|
|
904
1266
|
this.retryEntries.delete(retryEntry);
|
|
905
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1267
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
906
1268
|
return;
|
|
907
1269
|
}
|
|
908
1270
|
runner.onCancel = () => {
|
|
909
1271
|
const index = this.queue.indexOf(runner);
|
|
910
1272
|
if (index !== -1) {
|
|
911
1273
|
this.queue.splice(index, 1);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
}
|
|
917
1287
|
this.checkIdle();
|
|
918
1288
|
}
|
|
919
1289
|
};
|
|
920
|
-
this.
|
|
1290
|
+
this.insertIntoQueue(runner);
|
|
921
1291
|
this.pump();
|
|
922
1292
|
}, backoffDelay)
|
|
923
1293
|
};
|
|
@@ -926,11 +1296,19 @@ var TaskQueue = class {
|
|
|
926
1296
|
if (this.retryEntries.has(retryEntry)) {
|
|
927
1297
|
clearTimeout(retryEntry.timerId);
|
|
928
1298
|
this.retryEntries.delete(retryEntry);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
+
}
|
|
934
1312
|
this.checkIdle();
|
|
935
1313
|
}
|
|
936
1314
|
};
|
|
@@ -977,7 +1355,7 @@ var TaskQueue = class {
|
|
|
977
1355
|
clear() {
|
|
978
1356
|
while (this.queue.length > 0) {
|
|
979
1357
|
const runner = this.queue.shift();
|
|
980
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1358
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
981
1359
|
runner.cancel("Scheduler cleared");
|
|
982
1360
|
this.cancelledTasks++;
|
|
983
1361
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1027,7 +1405,9 @@ var TaskQueue = class {
|
|
|
1027
1405
|
timedOutTasks: this.timedOutTasks,
|
|
1028
1406
|
retriedTasks: this.retriedTasks,
|
|
1029
1407
|
totalDispatched: this.totalDispatched,
|
|
1030
|
-
capacity: this.concurrency
|
|
1408
|
+
capacity: this.concurrency,
|
|
1409
|
+
isPaused: this._isPaused,
|
|
1410
|
+
circuitState: this.circuitBreakerCoordinator?.state
|
|
1031
1411
|
});
|
|
1032
1412
|
}
|
|
1033
1413
|
};
|
|
@@ -1098,6 +1478,8 @@ var TaskRunner = class {
|
|
|
1098
1478
|
}
|
|
1099
1479
|
}
|
|
1100
1480
|
}
|
|
1481
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1482
|
+
totalTimedOut = false;
|
|
1101
1483
|
/**
|
|
1102
1484
|
* Gets the current lifecycle state of the task.
|
|
1103
1485
|
*/
|
|
@@ -1130,7 +1512,7 @@ var TaskRunner = class {
|
|
|
1130
1512
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1131
1513
|
*/
|
|
1132
1514
|
async canRetry(error, retryOptions) {
|
|
1133
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1515
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1134
1516
|
return false;
|
|
1135
1517
|
}
|
|
1136
1518
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1171,15 +1553,16 @@ var TaskRunner = class {
|
|
|
1171
1553
|
let abortListener;
|
|
1172
1554
|
const abortPromise = new Promise((_, reject) => {
|
|
1173
1555
|
abortListener = () => {
|
|
1174
|
-
|
|
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 */;
|
|
1175
1559
|
reject(
|
|
1176
|
-
new AhkoTimeoutError(
|
|
1560
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1177
1561
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1178
1562
|
{ timeoutMs: this.timeoutMs }
|
|
1179
1563
|
)
|
|
1180
1564
|
);
|
|
1181
1565
|
} else {
|
|
1182
|
-
const reason = this.abortController.signal.reason;
|
|
1183
1566
|
reject(
|
|
1184
1567
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1185
1568
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1214,6 +1597,10 @@ var TaskRunner = class {
|
|
|
1214
1597
|
}
|
|
1215
1598
|
taskExecutionPromise.catch(() => {
|
|
1216
1599
|
});
|
|
1600
|
+
abortPromise.catch(() => {
|
|
1601
|
+
});
|
|
1602
|
+
timeoutPromise?.catch(() => {
|
|
1603
|
+
});
|
|
1217
1604
|
const racePromises = [
|
|
1218
1605
|
taskExecutionPromise,
|
|
1219
1606
|
abortPromise
|
|
@@ -1305,6 +1692,31 @@ var TaskRunner = class {
|
|
|
1305
1692
|
this.onCancel?.(this);
|
|
1306
1693
|
}
|
|
1307
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
|
+
}
|
|
1308
1720
|
/**
|
|
1309
1721
|
* Handles external AbortSignal trigger.
|
|
1310
1722
|
*/
|
|
@@ -1323,9 +1735,55 @@ var TaskRunner = class {
|
|
|
1323
1735
|
};
|
|
1324
1736
|
|
|
1325
1737
|
// src/ahko.ts
|
|
1326
|
-
var Ahko = class {
|
|
1738
|
+
var Ahko = class _Ahko {
|
|
1327
1739
|
/** Internal queue and concurrency manager */
|
|
1328
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
|
+
}
|
|
1329
1787
|
/**
|
|
1330
1788
|
* Initializes a new Ahko scheduler instance.
|
|
1331
1789
|
*
|
|
@@ -1338,79 +1796,152 @@ var Ahko = class {
|
|
|
1338
1796
|
* ```
|
|
1339
1797
|
*/
|
|
1340
1798
|
constructor(options) {
|
|
1341
|
-
|
|
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
|
+
};
|
|
1342
1871
|
}
|
|
1343
1872
|
/**
|
|
1344
1873
|
* Schedules a task for execution with full return type inference.
|
|
1345
1874
|
*
|
|
1346
1875
|
* @template T - Inferred return type of the task.
|
|
1347
1876
|
* @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
|
|
1348
|
-
* @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.
|
|
1349
1878
|
* @returns A promise that resolves with the task's return value.
|
|
1350
1879
|
*
|
|
1351
1880
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1352
1881
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1353
|
-
* @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.
|
|
1354
1884
|
*
|
|
1355
1885
|
* @example
|
|
1356
1886
|
* ```typescript
|
|
1357
1887
|
* // Immediate execution (subject to concurrency)
|
|
1358
1888
|
* const count = await ahko.schedule(async () => 42);
|
|
1359
1889
|
*
|
|
1360
|
-
* //
|
|
1361
|
-
* await ahko.schedule(
|
|
1362
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1363
|
-
* { strategy: "delay", delay: 1000 }
|
|
1364
|
-
* );
|
|
1890
|
+
* // High priority task
|
|
1891
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1365
1892
|
* ```
|
|
1366
1893
|
*/
|
|
1367
1894
|
schedule(task, options) {
|
|
1368
1895
|
if (typeof task !== "function") {
|
|
1369
1896
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1370
1897
|
}
|
|
1371
|
-
const
|
|
1898
|
+
const mergedOptions = {
|
|
1899
|
+
...this.defaultScheduleOptions,
|
|
1900
|
+
...options
|
|
1901
|
+
};
|
|
1902
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1372
1903
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1373
|
-
if (!
|
|
1904
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1374
1905
|
throw new AhkoConfigurationError(
|
|
1375
1906
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1376
1907
|
);
|
|
1377
1908
|
}
|
|
1378
|
-
const waitMs =
|
|
1909
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1379
1910
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1380
1911
|
throw new AhkoConfigurationError(
|
|
1381
1912
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1382
1913
|
);
|
|
1383
1914
|
}
|
|
1384
1915
|
return this.queue.debounceCoordinator.schedule(
|
|
1385
|
-
|
|
1916
|
+
mergedOptions.key,
|
|
1386
1917
|
task,
|
|
1387
1918
|
waitMs,
|
|
1388
|
-
|
|
1919
|
+
mergedOptions,
|
|
1389
1920
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1390
1921
|
);
|
|
1391
1922
|
}
|
|
1392
1923
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1393
|
-
if (!
|
|
1924
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1394
1925
|
throw new AhkoConfigurationError(
|
|
1395
1926
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1396
1927
|
);
|
|
1397
1928
|
}
|
|
1398
|
-
const waitMs =
|
|
1929
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1399
1930
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1400
1931
|
throw new AhkoConfigurationError(
|
|
1401
1932
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1402
1933
|
);
|
|
1403
1934
|
}
|
|
1404
1935
|
return this.queue.throttleCoordinator.schedule(
|
|
1405
|
-
|
|
1936
|
+
mergedOptions.key,
|
|
1406
1937
|
task,
|
|
1407
1938
|
waitMs,
|
|
1408
|
-
|
|
1939
|
+
mergedOptions,
|
|
1409
1940
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1410
1941
|
);
|
|
1411
1942
|
}
|
|
1412
|
-
const runner = new TaskRunner(task,
|
|
1413
|
-
return this.queue.enqueue(runner,
|
|
1943
|
+
const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
|
|
1944
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1414
1945
|
}
|
|
1415
1946
|
/**
|
|
1416
1947
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1473,12 +2004,12 @@ var Ahko = class {
|
|
|
1473
2004
|
/**
|
|
1474
2005
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1475
2006
|
*
|
|
1476
|
-
* @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.
|
|
1477
2008
|
*
|
|
1478
2009
|
* @example
|
|
1479
2010
|
* ```typescript
|
|
1480
2011
|
* const stats = ahko.stats();
|
|
1481
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2012
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1482
2013
|
* ```
|
|
1483
2014
|
*/
|
|
1484
2015
|
stats() {
|
|
@@ -1564,7 +2095,7 @@ var Ahko = class {
|
|
|
1564
2095
|
};
|
|
1565
2096
|
|
|
1566
2097
|
// src/version.ts
|
|
1567
|
-
var VERSION = "1.
|
|
2098
|
+
var VERSION = "1.1.0";
|
|
1568
2099
|
|
|
1569
2100
|
// src/errors/queue.error.ts
|
|
1570
2101
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1638,16 +2169,26 @@ function combineSignals(signals) {
|
|
|
1638
2169
|
export {
|
|
1639
2170
|
Ahko,
|
|
1640
2171
|
AhkoCancellationError,
|
|
2172
|
+
AhkoCircuitBreakerOpenError,
|
|
1641
2173
|
AhkoConfigurationError,
|
|
1642
2174
|
AhkoError,
|
|
1643
2175
|
AhkoQueueError,
|
|
1644
2176
|
AhkoTimeoutError,
|
|
2177
|
+
CircuitBreakerCoordinator,
|
|
1645
2178
|
DEFAULT_BASE_DELAY,
|
|
1646
2179
|
DEFAULT_MAX_DELAY,
|
|
2180
|
+
ECircuitState,
|
|
1647
2181
|
EScheduleStrategy,
|
|
1648
2182
|
ETaskState,
|
|
2183
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1649
2184
|
VERSION,
|
|
1650
2185
|
calculateBackoff,
|
|
1651
|
-
combineSignals
|
|
2186
|
+
combineSignals,
|
|
2187
|
+
getActiveConfig,
|
|
2188
|
+
getProfileConfig,
|
|
2189
|
+
loadConfig,
|
|
2190
|
+
loadConfigFile,
|
|
2191
|
+
resetConfig,
|
|
2192
|
+
resolvePriorityWeight
|
|
1652
2193
|
};
|
|
1653
2194
|
//# sourceMappingURL=index.js.map
|