@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.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
|
/**
|
|
@@ -566,6 +808,10 @@ var TaskQueue = class {
|
|
|
566
808
|
throttleCoordinator = new ThrottleCoordinator();
|
|
567
809
|
/** Lifecycle event emitter for task and scheduler events */
|
|
568
810
|
emitter = new AhkoEventEmitter();
|
|
811
|
+
/** Circuit breaker coordinator if configured */
|
|
812
|
+
circuitBreakerCoordinator;
|
|
813
|
+
/** Pause state flag */
|
|
814
|
+
_isPaused = false;
|
|
569
815
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
570
816
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
571
817
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -587,9 +833,10 @@ var TaskQueue = class {
|
|
|
587
833
|
*
|
|
588
834
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
589
835
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
836
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
590
837
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
591
838
|
*/
|
|
592
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
839
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
|
|
593
840
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
594
841
|
throw new AhkoConfigurationError(
|
|
595
842
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -602,9 +849,51 @@ var TaskQueue = class {
|
|
|
602
849
|
}
|
|
603
850
|
this.concurrency = concurrency;
|
|
604
851
|
this.minIntervalMs = minIntervalMs;
|
|
852
|
+
if (circuitBreakerOptions) {
|
|
853
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
854
|
+
}
|
|
605
855
|
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
606
856
|
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
607
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);
|
|
896
|
+
}
|
|
608
897
|
/**
|
|
609
898
|
* Enqueues a task runner according to the specified schedule options.
|
|
610
899
|
*
|
|
@@ -658,6 +947,13 @@ var TaskQueue = class {
|
|
|
658
947
|
);
|
|
659
948
|
}
|
|
660
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
|
+
}
|
|
661
957
|
if (options) {
|
|
662
958
|
this.runnerOptions.set(runner, options);
|
|
663
959
|
}
|
|
@@ -665,6 +961,16 @@ var TaskQueue = class {
|
|
|
665
961
|
this.cancelledTasks++;
|
|
666
962
|
return runner.promise;
|
|
667
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
|
+
}
|
|
668
974
|
if (strategy === "delay" /* DELAY */) {
|
|
669
975
|
const delayMs = options?.delay ?? 0;
|
|
670
976
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -688,15 +994,23 @@ var TaskQueue = class {
|
|
|
688
994
|
const index = this.queue.indexOf(runner);
|
|
689
995
|
if (index !== -1) {
|
|
690
996
|
this.queue.splice(index, 1);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
+
}
|
|
696
1010
|
this.checkIdle();
|
|
697
1011
|
}
|
|
698
1012
|
};
|
|
699
|
-
this.
|
|
1013
|
+
this.insertIntoQueue(runner);
|
|
700
1014
|
this.pump();
|
|
701
1015
|
return runner.promise;
|
|
702
1016
|
}
|
|
@@ -709,22 +1023,30 @@ var TaskQueue = class {
|
|
|
709
1023
|
runner,
|
|
710
1024
|
timerId: setTimeout(() => {
|
|
711
1025
|
this.delayedEntries.delete(delayedEntry);
|
|
712
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1026
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
713
1027
|
return;
|
|
714
1028
|
}
|
|
715
1029
|
runner.onCancel = () => {
|
|
716
1030
|
const index = this.queue.indexOf(runner);
|
|
717
1031
|
if (index !== -1) {
|
|
718
1032
|
this.queue.splice(index, 1);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
}
|
|
724
1046
|
this.checkIdle();
|
|
725
1047
|
}
|
|
726
1048
|
};
|
|
727
|
-
this.
|
|
1049
|
+
this.insertIntoQueue(runner);
|
|
728
1050
|
this.pump();
|
|
729
1051
|
}, delayMs)
|
|
730
1052
|
};
|
|
@@ -733,11 +1055,19 @@ var TaskQueue = class {
|
|
|
733
1055
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
734
1056
|
clearTimeout(delayedEntry.timerId);
|
|
735
1057
|
this.delayedEntries.delete(delayedEntry);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|
|
741
1071
|
this.checkIdle();
|
|
742
1072
|
}
|
|
743
1073
|
};
|
|
@@ -750,22 +1080,30 @@ var TaskQueue = class {
|
|
|
750
1080
|
let idleEntry;
|
|
751
1081
|
const handle = IdleScheduler.schedule(() => {
|
|
752
1082
|
this.idleEntries.delete(idleEntry);
|
|
753
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1083
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
754
1084
|
return;
|
|
755
1085
|
}
|
|
756
1086
|
runner.onCancel = () => {
|
|
757
1087
|
const index = this.queue.indexOf(runner);
|
|
758
1088
|
if (index !== -1) {
|
|
759
1089
|
this.queue.splice(index, 1);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
+
}
|
|
765
1103
|
this.checkIdle();
|
|
766
1104
|
}
|
|
767
1105
|
};
|
|
768
|
-
this.
|
|
1106
|
+
this.insertIntoQueue(runner);
|
|
769
1107
|
this.pump();
|
|
770
1108
|
}, idleTimeout);
|
|
771
1109
|
idleEntry = { runner, handle };
|
|
@@ -774,21 +1112,30 @@ var TaskQueue = class {
|
|
|
774
1112
|
if (this.idleEntries.has(idleEntry)) {
|
|
775
1113
|
handle.cancel();
|
|
776
1114
|
this.idleEntries.delete(idleEntry);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
+
}
|
|
782
1128
|
this.checkIdle();
|
|
783
1129
|
}
|
|
784
1130
|
};
|
|
785
1131
|
}
|
|
786
1132
|
/**
|
|
787
1133
|
* Pumps the queue by picking pending tasks and executing them
|
|
788
|
-
* as long as concurrency capacity is available
|
|
1134
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1135
|
+
* and queue is not paused.
|
|
789
1136
|
*/
|
|
790
1137
|
pump() {
|
|
791
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
1138
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
792
1139
|
return;
|
|
793
1140
|
}
|
|
794
1141
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -805,7 +1152,7 @@ var TaskQueue = class {
|
|
|
805
1152
|
return;
|
|
806
1153
|
}
|
|
807
1154
|
}
|
|
808
|
-
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
1155
|
+
while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
809
1156
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
810
1157
|
const now = Date.now();
|
|
811
1158
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -824,9 +1171,25 @@ var TaskQueue = class {
|
|
|
824
1171
|
if (!runner) {
|
|
825
1172
|
break;
|
|
826
1173
|
}
|
|
827
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1174
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
828
1175
|
continue;
|
|
829
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
|
+
}
|
|
830
1193
|
this.activeRunners.add(runner);
|
|
831
1194
|
this.lastTaskStartTime = Date.now();
|
|
832
1195
|
void this.executeRunner(runner);
|
|
@@ -855,6 +1218,7 @@ var TaskQueue = class {
|
|
|
855
1218
|
});
|
|
856
1219
|
try {
|
|
857
1220
|
const result = await runner.run();
|
|
1221
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
858
1222
|
this.completedTasks++;
|
|
859
1223
|
this.activeRunners.delete(runner);
|
|
860
1224
|
this.runnerOptions.delete(runner);
|
|
@@ -890,11 +1254,14 @@ var TaskQueue = class {
|
|
|
890
1254
|
this.scheduleRetry(runner, options);
|
|
891
1255
|
return;
|
|
892
1256
|
}
|
|
1257
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1258
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1259
|
+
}
|
|
893
1260
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
894
1261
|
this.timedOutTasks++;
|
|
895
1262
|
this.emitter.emit("task:timeout", {
|
|
896
1263
|
taskId: runner.taskId,
|
|
897
|
-
timeoutMs: runner.timeoutMs
|
|
1264
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
898
1265
|
});
|
|
899
1266
|
} else {
|
|
900
1267
|
this.failedTasks++;
|
|
@@ -924,15 +1291,23 @@ var TaskQueue = class {
|
|
|
924
1291
|
const index = this.queue.indexOf(runner);
|
|
925
1292
|
if (index !== -1) {
|
|
926
1293
|
this.queue.splice(index, 1);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1307
|
this.checkIdle();
|
|
933
1308
|
}
|
|
934
1309
|
};
|
|
935
|
-
this.
|
|
1310
|
+
this.insertIntoQueue(runner);
|
|
936
1311
|
this.pump();
|
|
937
1312
|
return;
|
|
938
1313
|
}
|
|
@@ -940,22 +1315,30 @@ var TaskQueue = class {
|
|
|
940
1315
|
runner,
|
|
941
1316
|
timerId: setTimeout(() => {
|
|
942
1317
|
this.retryEntries.delete(retryEntry);
|
|
943
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1318
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
944
1319
|
return;
|
|
945
1320
|
}
|
|
946
1321
|
runner.onCancel = () => {
|
|
947
1322
|
const index = this.queue.indexOf(runner);
|
|
948
1323
|
if (index !== -1) {
|
|
949
1324
|
this.queue.splice(index, 1);
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
+
}
|
|
955
1338
|
this.checkIdle();
|
|
956
1339
|
}
|
|
957
1340
|
};
|
|
958
|
-
this.
|
|
1341
|
+
this.insertIntoQueue(runner);
|
|
959
1342
|
this.pump();
|
|
960
1343
|
}, backoffDelay)
|
|
961
1344
|
};
|
|
@@ -964,11 +1347,19 @@ var TaskQueue = class {
|
|
|
964
1347
|
if (this.retryEntries.has(retryEntry)) {
|
|
965
1348
|
clearTimeout(retryEntry.timerId);
|
|
966
1349
|
this.retryEntries.delete(retryEntry);
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
+
}
|
|
972
1363
|
this.checkIdle();
|
|
973
1364
|
}
|
|
974
1365
|
};
|
|
@@ -1015,7 +1406,7 @@ var TaskQueue = class {
|
|
|
1015
1406
|
clear() {
|
|
1016
1407
|
while (this.queue.length > 0) {
|
|
1017
1408
|
const runner = this.queue.shift();
|
|
1018
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1409
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
1019
1410
|
runner.cancel("Scheduler cleared");
|
|
1020
1411
|
this.cancelledTasks++;
|
|
1021
1412
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1065,7 +1456,9 @@ var TaskQueue = class {
|
|
|
1065
1456
|
timedOutTasks: this.timedOutTasks,
|
|
1066
1457
|
retriedTasks: this.retriedTasks,
|
|
1067
1458
|
totalDispatched: this.totalDispatched,
|
|
1068
|
-
capacity: this.concurrency
|
|
1459
|
+
capacity: this.concurrency,
|
|
1460
|
+
isPaused: this._isPaused,
|
|
1461
|
+
circuitState: this.circuitBreakerCoordinator?.state
|
|
1069
1462
|
});
|
|
1070
1463
|
}
|
|
1071
1464
|
};
|
|
@@ -1136,6 +1529,8 @@ var TaskRunner = class {
|
|
|
1136
1529
|
}
|
|
1137
1530
|
}
|
|
1138
1531
|
}
|
|
1532
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1533
|
+
totalTimedOut = false;
|
|
1139
1534
|
/**
|
|
1140
1535
|
* Gets the current lifecycle state of the task.
|
|
1141
1536
|
*/
|
|
@@ -1168,7 +1563,7 @@ var TaskRunner = class {
|
|
|
1168
1563
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1169
1564
|
*/
|
|
1170
1565
|
async canRetry(error, retryOptions) {
|
|
1171
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1566
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1172
1567
|
return false;
|
|
1173
1568
|
}
|
|
1174
1569
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1209,15 +1604,16 @@ var TaskRunner = class {
|
|
|
1209
1604
|
let abortListener;
|
|
1210
1605
|
const abortPromise = new Promise((_, reject) => {
|
|
1211
1606
|
abortListener = () => {
|
|
1212
|
-
|
|
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 */;
|
|
1213
1610
|
reject(
|
|
1214
|
-
new AhkoTimeoutError(
|
|
1611
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1215
1612
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1216
1613
|
{ timeoutMs: this.timeoutMs }
|
|
1217
1614
|
)
|
|
1218
1615
|
);
|
|
1219
1616
|
} else {
|
|
1220
|
-
const reason = this.abortController.signal.reason;
|
|
1221
1617
|
reject(
|
|
1222
1618
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1223
1619
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1252,6 +1648,10 @@ var TaskRunner = class {
|
|
|
1252
1648
|
}
|
|
1253
1649
|
taskExecutionPromise.catch(() => {
|
|
1254
1650
|
});
|
|
1651
|
+
abortPromise.catch(() => {
|
|
1652
|
+
});
|
|
1653
|
+
timeoutPromise?.catch(() => {
|
|
1654
|
+
});
|
|
1255
1655
|
const racePromises = [
|
|
1256
1656
|
taskExecutionPromise,
|
|
1257
1657
|
abortPromise
|
|
@@ -1343,6 +1743,31 @@ var TaskRunner = class {
|
|
|
1343
1743
|
this.onCancel?.(this);
|
|
1344
1744
|
}
|
|
1345
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
|
+
}
|
|
1346
1771
|
/**
|
|
1347
1772
|
* Handles external AbortSignal trigger.
|
|
1348
1773
|
*/
|
|
@@ -1361,9 +1786,55 @@ var TaskRunner = class {
|
|
|
1361
1786
|
};
|
|
1362
1787
|
|
|
1363
1788
|
// src/ahko.ts
|
|
1364
|
-
var Ahko = class {
|
|
1789
|
+
var Ahko = class _Ahko {
|
|
1365
1790
|
/** Internal queue and concurrency manager */
|
|
1366
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
|
+
}
|
|
1367
1838
|
/**
|
|
1368
1839
|
* Initializes a new Ahko scheduler instance.
|
|
1369
1840
|
*
|
|
@@ -1376,79 +1847,152 @@ var Ahko = class {
|
|
|
1376
1847
|
* ```
|
|
1377
1848
|
*/
|
|
1378
1849
|
constructor(options) {
|
|
1379
|
-
|
|
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
|
+
};
|
|
1380
1922
|
}
|
|
1381
1923
|
/**
|
|
1382
1924
|
* Schedules a task for execution with full return type inference.
|
|
1383
1925
|
*
|
|
1384
1926
|
* @template T - Inferred return type of the task.
|
|
1385
1927
|
* @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
|
|
1386
|
-
* @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.
|
|
1387
1929
|
* @returns A promise that resolves with the task's return value.
|
|
1388
1930
|
*
|
|
1389
1931
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1390
1932
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1391
|
-
* @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.
|
|
1392
1935
|
*
|
|
1393
1936
|
* @example
|
|
1394
1937
|
* ```typescript
|
|
1395
1938
|
* // Immediate execution (subject to concurrency)
|
|
1396
1939
|
* const count = await ahko.schedule(async () => 42);
|
|
1397
1940
|
*
|
|
1398
|
-
* //
|
|
1399
|
-
* await ahko.schedule(
|
|
1400
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1401
|
-
* { strategy: "delay", delay: 1000 }
|
|
1402
|
-
* );
|
|
1941
|
+
* // High priority task
|
|
1942
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1403
1943
|
* ```
|
|
1404
1944
|
*/
|
|
1405
1945
|
schedule(task, options) {
|
|
1406
1946
|
if (typeof task !== "function") {
|
|
1407
1947
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1408
1948
|
}
|
|
1409
|
-
const
|
|
1949
|
+
const mergedOptions = {
|
|
1950
|
+
...this.defaultScheduleOptions,
|
|
1951
|
+
...options
|
|
1952
|
+
};
|
|
1953
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1410
1954
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1411
|
-
if (!
|
|
1955
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1412
1956
|
throw new AhkoConfigurationError(
|
|
1413
1957
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1414
1958
|
);
|
|
1415
1959
|
}
|
|
1416
|
-
const waitMs =
|
|
1960
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1417
1961
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1418
1962
|
throw new AhkoConfigurationError(
|
|
1419
1963
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1420
1964
|
);
|
|
1421
1965
|
}
|
|
1422
1966
|
return this.queue.debounceCoordinator.schedule(
|
|
1423
|
-
|
|
1967
|
+
mergedOptions.key,
|
|
1424
1968
|
task,
|
|
1425
1969
|
waitMs,
|
|
1426
|
-
|
|
1970
|
+
mergedOptions,
|
|
1427
1971
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1428
1972
|
);
|
|
1429
1973
|
}
|
|
1430
1974
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1431
|
-
if (!
|
|
1975
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1432
1976
|
throw new AhkoConfigurationError(
|
|
1433
1977
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1434
1978
|
);
|
|
1435
1979
|
}
|
|
1436
|
-
const waitMs =
|
|
1980
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1437
1981
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1438
1982
|
throw new AhkoConfigurationError(
|
|
1439
1983
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1440
1984
|
);
|
|
1441
1985
|
}
|
|
1442
1986
|
return this.queue.throttleCoordinator.schedule(
|
|
1443
|
-
|
|
1987
|
+
mergedOptions.key,
|
|
1444
1988
|
task,
|
|
1445
1989
|
waitMs,
|
|
1446
|
-
|
|
1990
|
+
mergedOptions,
|
|
1447
1991
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1448
1992
|
);
|
|
1449
1993
|
}
|
|
1450
|
-
const runner = new TaskRunner(task,
|
|
1451
|
-
return this.queue.enqueue(runner,
|
|
1994
|
+
const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
|
|
1995
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1452
1996
|
}
|
|
1453
1997
|
/**
|
|
1454
1998
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1511,12 +2055,12 @@ var Ahko = class {
|
|
|
1511
2055
|
/**
|
|
1512
2056
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1513
2057
|
*
|
|
1514
|
-
* @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.
|
|
1515
2059
|
*
|
|
1516
2060
|
* @example
|
|
1517
2061
|
* ```typescript
|
|
1518
2062
|
* const stats = ahko.stats();
|
|
1519
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2063
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1520
2064
|
* ```
|
|
1521
2065
|
*/
|
|
1522
2066
|
stats() {
|
|
@@ -1602,7 +2146,7 @@ var Ahko = class {
|
|
|
1602
2146
|
};
|
|
1603
2147
|
|
|
1604
2148
|
// src/version.ts
|
|
1605
|
-
var VERSION = "1.
|
|
2149
|
+
var VERSION = "1.1.0";
|
|
1606
2150
|
|
|
1607
2151
|
// src/errors/queue.error.ts
|
|
1608
2152
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1677,16 +2221,26 @@ function combineSignals(signals) {
|
|
|
1677
2221
|
0 && (module.exports = {
|
|
1678
2222
|
Ahko,
|
|
1679
2223
|
AhkoCancellationError,
|
|
2224
|
+
AhkoCircuitBreakerOpenError,
|
|
1680
2225
|
AhkoConfigurationError,
|
|
1681
2226
|
AhkoError,
|
|
1682
2227
|
AhkoQueueError,
|
|
1683
2228
|
AhkoTimeoutError,
|
|
2229
|
+
CircuitBreakerCoordinator,
|
|
1684
2230
|
DEFAULT_BASE_DELAY,
|
|
1685
2231
|
DEFAULT_MAX_DELAY,
|
|
2232
|
+
ECircuitState,
|
|
1686
2233
|
EScheduleStrategy,
|
|
1687
2234
|
ETaskState,
|
|
2235
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1688
2236
|
VERSION,
|
|
1689
2237
|
calculateBackoff,
|
|
1690
|
-
combineSignals
|
|
2238
|
+
combineSignals,
|
|
2239
|
+
getActiveConfig,
|
|
2240
|
+
getProfileConfig,
|
|
2241
|
+
loadConfig,
|
|
2242
|
+
loadConfigFile,
|
|
2243
|
+
resetConfig,
|
|
2244
|
+
resolvePriorityWeight
|
|
1691
2245
|
});
|
|
1692
2246
|
//# sourceMappingURL=index.cjs.map
|