@mrjacket/ahko 1.0.0 → 1.1.5
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 +63 -0
- package/README.md +230 -7
- package/dist/ahko.d.ts +155 -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 +1116 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1101 -97
- package/dist/index.js.map +1 -1
- package/dist/models/adaptive.model.d.ts +47 -0
- package/dist/models/batch.model.d.ts +22 -0
- package/dist/models/circuit-breaker.model.d.ts +39 -0
- package/dist/models/config.model.d.ts +23 -0
- package/dist/models/events.model.d.ts +6 -0
- package/dist/models/index.d.ts +5 -0
- package/dist/models/options.model.d.ts +32 -0
- package/dist/models/priority.model.d.ts +18 -0
- package/dist/models/stats.model.d.ts +8 -0
- package/dist/scheduler/adaptive-coordinator.d.ts +45 -0
- package/dist/scheduler/circuit-breaker.d.ts +56 -0
- package/dist/scheduler/task-queue.d.ts +74 -5
- package/dist/scheduler/task-runner.d.ts +13 -1
- 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,24 +17,43 @@ 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
|
|
21
31
|
var src_exports = {};
|
|
22
32
|
__export(src_exports, {
|
|
33
|
+
AdaptiveCoordinator: () => AdaptiveCoordinator,
|
|
23
34
|
Ahko: () => Ahko,
|
|
24
35
|
AhkoCancellationError: () => AhkoCancellationError,
|
|
36
|
+
AhkoCircuitBreakerOpenError: () => AhkoCircuitBreakerOpenError,
|
|
25
37
|
AhkoConfigurationError: () => AhkoConfigurationError,
|
|
26
38
|
AhkoError: () => AhkoError,
|
|
27
39
|
AhkoQueueError: () => AhkoQueueError,
|
|
28
40
|
AhkoTimeoutError: () => AhkoTimeoutError,
|
|
41
|
+
CircuitBreakerCoordinator: () => CircuitBreakerCoordinator,
|
|
29
42
|
DEFAULT_BASE_DELAY: () => DEFAULT_BASE_DELAY,
|
|
30
43
|
DEFAULT_MAX_DELAY: () => DEFAULT_MAX_DELAY,
|
|
44
|
+
ECircuitState: () => ECircuitState,
|
|
31
45
|
EScheduleStrategy: () => EScheduleStrategy,
|
|
32
46
|
ETaskState: () => ETaskState,
|
|
47
|
+
TASK_PRIORITY_WEIGHTS: () => TASK_PRIORITY_WEIGHTS,
|
|
33
48
|
VERSION: () => VERSION,
|
|
34
49
|
calculateBackoff: () => calculateBackoff,
|
|
35
|
-
combineSignals: () => combineSignals
|
|
50
|
+
combineSignals: () => combineSignals,
|
|
51
|
+
getActiveConfig: () => getActiveConfig,
|
|
52
|
+
getProfileConfig: () => getProfileConfig,
|
|
53
|
+
loadConfig: () => loadConfig,
|
|
54
|
+
loadConfigFile: () => loadConfigFile,
|
|
55
|
+
resetConfig: () => resetConfig,
|
|
56
|
+
resolvePriorityWeight: () => resolvePriorityWeight
|
|
36
57
|
});
|
|
37
58
|
module.exports = __toCommonJS(src_exports);
|
|
38
59
|
|
|
@@ -51,6 +72,21 @@ var AhkoError = class extends Error {
|
|
|
51
72
|
}
|
|
52
73
|
};
|
|
53
74
|
|
|
75
|
+
// src/errors/cancellation.error.ts
|
|
76
|
+
var AhkoCancellationError = class extends AhkoError {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new AhkoCancellationError.
|
|
79
|
+
*
|
|
80
|
+
* @param message - Reason for cancellation.
|
|
81
|
+
* @param options - Standard Error options including cause.
|
|
82
|
+
*/
|
|
83
|
+
constructor(message = "Task was cancelled", options) {
|
|
84
|
+
super(message, options);
|
|
85
|
+
this.name = "AhkoCancellationError";
|
|
86
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
54
90
|
// src/errors/configuration.error.ts
|
|
55
91
|
var AhkoConfigurationError = class extends AhkoError {
|
|
56
92
|
/**
|
|
@@ -66,6 +102,72 @@ var AhkoConfigurationError = class extends AhkoError {
|
|
|
66
102
|
}
|
|
67
103
|
};
|
|
68
104
|
|
|
105
|
+
// src/config/config-loader.ts
|
|
106
|
+
var activeConfig;
|
|
107
|
+
function loadConfig(config) {
|
|
108
|
+
activeConfig = { ...config };
|
|
109
|
+
}
|
|
110
|
+
function resetConfig() {
|
|
111
|
+
activeConfig = void 0;
|
|
112
|
+
}
|
|
113
|
+
async function loadConfigFile(filePath = "config.ahko.json") {
|
|
114
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const { readFile } = await import("fs/promises");
|
|
119
|
+
const { resolve } = await import("path");
|
|
120
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
121
|
+
const content = await readFile(resolvedPath, "utf-8");
|
|
122
|
+
const parsed = JSON.parse(content);
|
|
123
|
+
activeConfig = parsed;
|
|
124
|
+
return parsed;
|
|
125
|
+
} catch {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function tryAutoDiscoverSync() {
|
|
130
|
+
if (activeConfig !== void 0 || typeof process === "undefined" || !process.versions?.node) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
let fs = null;
|
|
135
|
+
let path = null;
|
|
136
|
+
if (typeof process.getBuiltinModule === "function") {
|
|
137
|
+
const getBuiltin = process.getBuiltinModule;
|
|
138
|
+
fs = getBuiltin("node:fs");
|
|
139
|
+
path = getBuiltin("node:path");
|
|
140
|
+
} else if (typeof require === "function") {
|
|
141
|
+
fs = require("fs");
|
|
142
|
+
path = require("path");
|
|
143
|
+
}
|
|
144
|
+
if (fs && path) {
|
|
145
|
+
const configPath = path.resolve(process.cwd(), "config.ahko.json");
|
|
146
|
+
if (fs.existsSync(configPath)) {
|
|
147
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
148
|
+
activeConfig = JSON.parse(raw);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function getActiveConfig() {
|
|
155
|
+
if (activeConfig === void 0) {
|
|
156
|
+
tryAutoDiscoverSync();
|
|
157
|
+
}
|
|
158
|
+
return activeConfig;
|
|
159
|
+
}
|
|
160
|
+
function getProfileConfig(profileName) {
|
|
161
|
+
const config = getActiveConfig();
|
|
162
|
+
if (!config) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
if (profileName) {
|
|
166
|
+
return config.profiles?.[profileName];
|
|
167
|
+
}
|
|
168
|
+
return config.default;
|
|
169
|
+
}
|
|
170
|
+
|
|
69
171
|
// src/models/strategy.model.ts
|
|
70
172
|
var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
71
173
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
@@ -76,6 +178,23 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
76
178
|
return EScheduleStrategy2;
|
|
77
179
|
})(EScheduleStrategy || {});
|
|
78
180
|
|
|
181
|
+
// src/errors/circuit-breaker.error.ts
|
|
182
|
+
var AhkoCircuitBreakerOpenError = class extends AhkoError {
|
|
183
|
+
/** Time remaining in milliseconds before trial execution is allowed */
|
|
184
|
+
resetTimeoutMs;
|
|
185
|
+
/** Timestamp when the circuit tripped open */
|
|
186
|
+
trippedAt;
|
|
187
|
+
/** Total consecutive failures that caused the trip */
|
|
188
|
+
consecutiveFailures;
|
|
189
|
+
constructor(message = "Circuit breaker is open. Fast-failing task execution to protect downstream resources.", options) {
|
|
190
|
+
super(message);
|
|
191
|
+
this.name = "AhkoCircuitBreakerOpenError";
|
|
192
|
+
this.resetTimeoutMs = options?.resetTimeoutMs;
|
|
193
|
+
this.trippedAt = options?.trippedAt;
|
|
194
|
+
this.consecutiveFailures = options?.consecutiveFailures;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
79
198
|
// src/errors/timeout.error.ts
|
|
80
199
|
var AhkoTimeoutError = class extends AhkoError {
|
|
81
200
|
/**
|
|
@@ -96,6 +215,28 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
96
215
|
}
|
|
97
216
|
};
|
|
98
217
|
|
|
218
|
+
// src/models/priority.model.ts
|
|
219
|
+
var TASK_PRIORITY_WEIGHTS = {
|
|
220
|
+
high: 10,
|
|
221
|
+
normal: 0,
|
|
222
|
+
low: -10
|
|
223
|
+
};
|
|
224
|
+
function resolvePriorityWeight(priority) {
|
|
225
|
+
if (priority === void 0) {
|
|
226
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
227
|
+
}
|
|
228
|
+
if (typeof priority === "number") {
|
|
229
|
+
return Number.isFinite(priority) ? priority : TASK_PRIORITY_WEIGHTS.normal;
|
|
230
|
+
}
|
|
231
|
+
if (priority === "high") {
|
|
232
|
+
return TASK_PRIORITY_WEIGHTS.high;
|
|
233
|
+
}
|
|
234
|
+
if (priority === "low") {
|
|
235
|
+
return TASK_PRIORITY_WEIGHTS.low;
|
|
236
|
+
}
|
|
237
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
238
|
+
}
|
|
239
|
+
|
|
99
240
|
// src/models/state.model.ts
|
|
100
241
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
101
242
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -132,18 +273,251 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
132
273
|
return Math.floor(cappedDelay);
|
|
133
274
|
}
|
|
134
275
|
|
|
135
|
-
// src/
|
|
136
|
-
var
|
|
276
|
+
// src/scheduler/adaptive-coordinator.ts
|
|
277
|
+
var AdaptiveCoordinator = class {
|
|
278
|
+
_currentConcurrency;
|
|
279
|
+
minConcurrency;
|
|
280
|
+
maxConcurrency;
|
|
281
|
+
targetLatencyMs;
|
|
282
|
+
sampleWindowSize;
|
|
283
|
+
backoffFactor;
|
|
284
|
+
recentDurations = [];
|
|
285
|
+
lastAverageLatencyMs = 0;
|
|
286
|
+
onConcurrencyChange;
|
|
137
287
|
/**
|
|
138
|
-
*
|
|
288
|
+
* Initializes a new AdaptiveCoordinator instance.
|
|
139
289
|
*
|
|
140
|
-
* @param
|
|
141
|
-
* @param
|
|
290
|
+
* @param options - Adaptive concurrency configuration options.
|
|
291
|
+
* @param initialConcurrency - Starting scheduler concurrency limit.
|
|
292
|
+
* @param onConcurrencyChange - Callback invoked when concurrency changes.
|
|
293
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
142
294
|
*/
|
|
143
|
-
constructor(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
295
|
+
constructor(options, initialConcurrency, onConcurrencyChange) {
|
|
296
|
+
if (typeof options.targetLatencyMs !== "number" || Number.isNaN(options.targetLatencyMs) || !Number.isFinite(options.targetLatencyMs) || options.targetLatencyMs <= 0) {
|
|
297
|
+
throw new AhkoConfigurationError(
|
|
298
|
+
`Invalid targetLatencyMs "${options.targetLatencyMs}". targetLatencyMs must be a positive number greater than 0.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const min = options.minConcurrency ?? 1;
|
|
302
|
+
if (typeof min !== "number" || Number.isNaN(min) || min < 1 || !Number.isInteger(min)) {
|
|
303
|
+
throw new AhkoConfigurationError(
|
|
304
|
+
`Invalid minConcurrency "${min}". minConcurrency must be an integer greater than or equal to 1.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const defaultMax = Number.isFinite(initialConcurrency) ? Math.max(min, initialConcurrency * 2) : Math.max(min, 10);
|
|
308
|
+
const max = options.maxConcurrency ?? defaultMax;
|
|
309
|
+
if (typeof max !== "number" || Number.isNaN(max) || max < min || !Number.isInteger(max)) {
|
|
310
|
+
throw new AhkoConfigurationError(
|
|
311
|
+
`Invalid maxConcurrency "${max}". maxConcurrency must be an integer greater than or equal to minConcurrency (${min}).`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const windowSize = options.sampleWindowSize ?? 5;
|
|
315
|
+
if (typeof windowSize !== "number" || Number.isNaN(windowSize) || windowSize < 1 || !Number.isInteger(windowSize)) {
|
|
316
|
+
throw new AhkoConfigurationError(
|
|
317
|
+
`Invalid sampleWindowSize "${windowSize}". sampleWindowSize must be an integer greater than or equal to 1.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const factor = options.backoffFactor ?? 0.7;
|
|
321
|
+
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0.1 || factor >= 0.99) {
|
|
322
|
+
throw new AhkoConfigurationError(
|
|
323
|
+
`Invalid backoffFactor "${factor}". backoffFactor must be a number between 0.1 and 0.99.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
this.minConcurrency = min;
|
|
327
|
+
this.maxConcurrency = max;
|
|
328
|
+
this.targetLatencyMs = options.targetLatencyMs;
|
|
329
|
+
this.sampleWindowSize = windowSize;
|
|
330
|
+
this.backoffFactor = factor;
|
|
331
|
+
this.onConcurrencyChange = onConcurrencyChange;
|
|
332
|
+
const clampedInitial = Number.isFinite(initialConcurrency) ? Math.min(Math.max(initialConcurrency, min), max) : min;
|
|
333
|
+
this._currentConcurrency = clampedInitial;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Current effective concurrency limit dictated by the adaptive controller.
|
|
337
|
+
*/
|
|
338
|
+
get currentConcurrency() {
|
|
339
|
+
return this._currentConcurrency;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Manually overrides the current concurrency within [minConcurrency, maxConcurrency].
|
|
343
|
+
*
|
|
344
|
+
* @param concurrency - New concurrency limit to set.
|
|
345
|
+
*/
|
|
346
|
+
setConcurrency(concurrency) {
|
|
347
|
+
const clamped = Math.min(Math.max(concurrency, this.minConcurrency), this.maxConcurrency);
|
|
348
|
+
if (clamped !== this._currentConcurrency) {
|
|
349
|
+
const prev = this._currentConcurrency;
|
|
350
|
+
this._currentConcurrency = clamped;
|
|
351
|
+
this.onConcurrencyChange(prev, clamped, "Manual concurrency override");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Records a task execution duration sample and triggers AIMD adjustment if window is filled.
|
|
356
|
+
*
|
|
357
|
+
* @param durationMs - Execution duration in milliseconds of the completed task.
|
|
358
|
+
*/
|
|
359
|
+
recordDuration(durationMs) {
|
|
360
|
+
this.recentDurations.push(durationMs);
|
|
361
|
+
if (this.recentDurations.length < this.sampleWindowSize) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const total = this.recentDurations.reduce((sum, val) => sum + val, 0);
|
|
365
|
+
const average = total / this.recentDurations.length;
|
|
366
|
+
this.lastAverageLatencyMs = average;
|
|
367
|
+
this.recentDurations = [];
|
|
368
|
+
if (average > this.targetLatencyMs) {
|
|
369
|
+
const decreased = Math.max(
|
|
370
|
+
this.minConcurrency,
|
|
371
|
+
Math.floor(this._currentConcurrency * this.backoffFactor)
|
|
372
|
+
);
|
|
373
|
+
if (decreased !== this._currentConcurrency) {
|
|
374
|
+
const prev = this._currentConcurrency;
|
|
375
|
+
this._currentConcurrency = decreased;
|
|
376
|
+
this.onConcurrencyChange(
|
|
377
|
+
prev,
|
|
378
|
+
decreased,
|
|
379
|
+
`Average latency (${Math.round(average)}ms) exceeded target (${this.targetLatencyMs}ms). Scaled down.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
} else if (average < this.targetLatencyMs * 0.75) {
|
|
383
|
+
const increased = Math.min(this.maxConcurrency, this._currentConcurrency + 1);
|
|
384
|
+
if (increased !== this._currentConcurrency) {
|
|
385
|
+
const prev = this._currentConcurrency;
|
|
386
|
+
this._currentConcurrency = increased;
|
|
387
|
+
this.onConcurrencyChange(
|
|
388
|
+
prev,
|
|
389
|
+
increased,
|
|
390
|
+
`Average latency (${Math.round(average)}ms) below target threshold. Scaled up.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns a snapshot of adaptive telemetry metrics.
|
|
397
|
+
*/
|
|
398
|
+
getStats() {
|
|
399
|
+
return {
|
|
400
|
+
currentConcurrency: this._currentConcurrency,
|
|
401
|
+
averageLatencyMs: this.lastAverageLatencyMs,
|
|
402
|
+
samplesRecorded: this.recentDurations.length
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/models/circuit-breaker.model.ts
|
|
408
|
+
var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
|
|
409
|
+
ECircuitState2["CLOSED"] = "closed";
|
|
410
|
+
ECircuitState2["OPEN"] = "open";
|
|
411
|
+
ECircuitState2["HALF_OPEN"] = "half_open";
|
|
412
|
+
return ECircuitState2;
|
|
413
|
+
})(ECircuitState || {});
|
|
414
|
+
|
|
415
|
+
// src/scheduler/circuit-breaker.ts
|
|
416
|
+
var CircuitBreakerCoordinator = class {
|
|
417
|
+
_state = "closed" /* CLOSED */;
|
|
418
|
+
_consecutiveFailures = 0;
|
|
419
|
+
_lastFailureTime;
|
|
420
|
+
failureThreshold;
|
|
421
|
+
resetTimeoutMs;
|
|
422
|
+
/**
|
|
423
|
+
* Initializes a new CircuitBreakerCoordinator.
|
|
424
|
+
*
|
|
425
|
+
* @param options - Configuration options for threshold and cool-down window.
|
|
426
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
427
|
+
*/
|
|
428
|
+
constructor(options) {
|
|
429
|
+
if (typeof options.failureThreshold !== "number" || Number.isNaN(options.failureThreshold) || !Number.isInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
430
|
+
throw new AhkoConfigurationError(
|
|
431
|
+
`Invalid failureThreshold "${options.failureThreshold}". failureThreshold must be an integer greater than or equal to 1.`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (typeof options.resetTimeoutMs !== "number" || Number.isNaN(options.resetTimeoutMs) || !Number.isFinite(options.resetTimeoutMs) || options.resetTimeoutMs <= 0) {
|
|
435
|
+
throw new AhkoConfigurationError(
|
|
436
|
+
`Invalid resetTimeoutMs "${options.resetTimeoutMs}". resetTimeoutMs must be a positive finite number greater than 0.`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
this.failureThreshold = options.failureThreshold;
|
|
440
|
+
this.resetTimeoutMs = options.resetTimeoutMs;
|
|
441
|
+
}
|
|
442
|
+
/** Current state of the circuit breaker */
|
|
443
|
+
get state() {
|
|
444
|
+
this.refreshState();
|
|
445
|
+
return this._state;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Checks whether an execution is currently allowed.
|
|
449
|
+
* If the circuit is OPEN and cool-down has not elapsed, fast-fails immediately.
|
|
450
|
+
*
|
|
451
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit is currently OPEN.
|
|
452
|
+
*/
|
|
453
|
+
checkAllowed() {
|
|
454
|
+
this.refreshState();
|
|
455
|
+
if (this._state === "open" /* OPEN */) {
|
|
456
|
+
const remainingMs = this._lastFailureTime ? Math.max(0, this.resetTimeoutMs - (Date.now() - this._lastFailureTime)) : this.resetTimeoutMs;
|
|
457
|
+
throw new AhkoCircuitBreakerOpenError(
|
|
458
|
+
`Circuit breaker is open. Fast-failing task execution. Remaining cool-down: ${remainingMs}ms.`,
|
|
459
|
+
{
|
|
460
|
+
resetTimeoutMs: remainingMs,
|
|
461
|
+
trippedAt: this._lastFailureTime,
|
|
462
|
+
consecutiveFailures: this._consecutiveFailures
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Records a successful task execution.
|
|
469
|
+
* Heals HALF_OPEN state back to CLOSED and resets consecutive failure counters.
|
|
470
|
+
*/
|
|
471
|
+
recordSuccess() {
|
|
472
|
+
this._consecutiveFailures = 0;
|
|
473
|
+
this._state = "closed" /* CLOSED */;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Records a failed task execution.
|
|
477
|
+
* Trips CLOSED to OPEN when threshold is met, or re-trips HALF_OPEN immediately.
|
|
478
|
+
*
|
|
479
|
+
* @param _error - Optional error that caused the failure.
|
|
480
|
+
*/
|
|
481
|
+
recordFailure(_error) {
|
|
482
|
+
this._consecutiveFailures++;
|
|
483
|
+
this._lastFailureTime = Date.now();
|
|
484
|
+
if (this._state === "half_open" /* HALF_OPEN */) {
|
|
485
|
+
this._state = "open" /* OPEN */;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (this._consecutiveFailures >= this.failureThreshold) {
|
|
489
|
+
this._state = "open" /* OPEN */;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Evaluates if enough time has passed to transition from OPEN to HALF_OPEN.
|
|
494
|
+
*/
|
|
495
|
+
refreshState() {
|
|
496
|
+
if (this._state === "open" /* OPEN */ && this._lastFailureTime !== void 0) {
|
|
497
|
+
const elapsed = Date.now() - this._lastFailureTime;
|
|
498
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
499
|
+
this._state = "half_open" /* HALF_OPEN */;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Resets the circuit breaker back to initial CLOSED state.
|
|
505
|
+
*/
|
|
506
|
+
reset() {
|
|
507
|
+
this._state = "closed" /* CLOSED */;
|
|
508
|
+
this._consecutiveFailures = 0;
|
|
509
|
+
this._lastFailureTime = void 0;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Returns a snapshot of circuit breaker telemetry.
|
|
513
|
+
*/
|
|
514
|
+
getStats() {
|
|
515
|
+
this.refreshState();
|
|
516
|
+
return {
|
|
517
|
+
state: this._state,
|
|
518
|
+
consecutiveFailures: this._consecutiveFailures,
|
|
519
|
+
lastFailureTime: this._lastFailureTime
|
|
520
|
+
};
|
|
147
521
|
}
|
|
148
522
|
};
|
|
149
523
|
|
|
@@ -543,7 +917,7 @@ var AhkoEventEmitter = class {
|
|
|
543
917
|
// src/scheduler/task-queue.ts
|
|
544
918
|
var TaskQueue = class {
|
|
545
919
|
/** Maximum concurrent active tasks */
|
|
546
|
-
|
|
920
|
+
_concurrency;
|
|
547
921
|
/** Minimum interval in milliseconds between consecutive task starts */
|
|
548
922
|
minIntervalMs;
|
|
549
923
|
/** Timestamp of the most recent task start */
|
|
@@ -560,12 +934,20 @@ var TaskQueue = class {
|
|
|
560
934
|
idleEntries = /* @__PURE__ */ new Set();
|
|
561
935
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
562
936
|
retryEntries = /* @__PURE__ */ new Set();
|
|
937
|
+
/** Tag index for selective cancellation and task classification */
|
|
938
|
+
tagIndex = /* @__PURE__ */ new Map();
|
|
563
939
|
/** Coordinator for debounced tasks with key coalescing */
|
|
564
940
|
debounceCoordinator = new DebounceCoordinator();
|
|
565
941
|
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
566
942
|
throttleCoordinator = new ThrottleCoordinator();
|
|
567
943
|
/** Lifecycle event emitter for task and scheduler events */
|
|
568
944
|
emitter = new AhkoEventEmitter();
|
|
945
|
+
/** Circuit breaker coordinator if configured */
|
|
946
|
+
circuitBreakerCoordinator;
|
|
947
|
+
/** Adaptive concurrency coordinator if configured */
|
|
948
|
+
adaptiveCoordinator;
|
|
949
|
+
/** Pause state flag */
|
|
950
|
+
_isPaused = false;
|
|
569
951
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
570
952
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
571
953
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -587,9 +969,11 @@ var TaskQueue = class {
|
|
|
587
969
|
*
|
|
588
970
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
589
971
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
972
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
973
|
+
* @param adaptiveOptions - Optional adaptive concurrency policy configuration.
|
|
590
974
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
591
975
|
*/
|
|
592
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
976
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions, adaptiveOptions) {
|
|
593
977
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
594
978
|
throw new AhkoConfigurationError(
|
|
595
979
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -600,11 +984,171 @@ var TaskQueue = class {
|
|
|
600
984
|
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
601
985
|
);
|
|
602
986
|
}
|
|
603
|
-
this.
|
|
987
|
+
this._concurrency = concurrency;
|
|
604
988
|
this.minIntervalMs = minIntervalMs;
|
|
989
|
+
if (circuitBreakerOptions) {
|
|
990
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
991
|
+
}
|
|
992
|
+
if (adaptiveOptions) {
|
|
993
|
+
this.adaptiveCoordinator = new AdaptiveCoordinator(
|
|
994
|
+
adaptiveOptions,
|
|
995
|
+
this._concurrency,
|
|
996
|
+
(previous, current, reason) => {
|
|
997
|
+
this._concurrency = current;
|
|
998
|
+
this.emitter.emit("concurrency:change", {
|
|
999
|
+
previousConcurrency: previous,
|
|
1000
|
+
currentConcurrency: current,
|
|
1001
|
+
reason
|
|
1002
|
+
});
|
|
1003
|
+
this.pump();
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
this._concurrency = this.adaptiveCoordinator.currentConcurrency;
|
|
1007
|
+
}
|
|
605
1008
|
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
606
1009
|
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
607
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Current concurrency capacity limit.
|
|
1013
|
+
*/
|
|
1014
|
+
get concurrency() {
|
|
1015
|
+
return this._concurrency;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Dynamically adjusts the concurrency limit at runtime.
|
|
1019
|
+
*
|
|
1020
|
+
* @param newConcurrency - New maximum concurrency (must be >= 1).
|
|
1021
|
+
* @throws {AhkoConfigurationError} If newConcurrency is less than 1.
|
|
1022
|
+
*/
|
|
1023
|
+
setConcurrency(newConcurrency) {
|
|
1024
|
+
if (Number.isNaN(newConcurrency) || newConcurrency < 1) {
|
|
1025
|
+
throw new AhkoConfigurationError(
|
|
1026
|
+
`Invalid concurrency "${newConcurrency}". Must be a number greater than or equal to 1.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const previous = this._concurrency;
|
|
1030
|
+
this._concurrency = newConcurrency;
|
|
1031
|
+
if (this.adaptiveCoordinator) {
|
|
1032
|
+
this.adaptiveCoordinator.setConcurrency(newConcurrency);
|
|
1033
|
+
}
|
|
1034
|
+
if (newConcurrency !== previous) {
|
|
1035
|
+
this.emitter.emit("concurrency:change", {
|
|
1036
|
+
previousConcurrency: previous,
|
|
1037
|
+
currentConcurrency: newConcurrency,
|
|
1038
|
+
reason: "Manual concurrency update"
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
this.pump();
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
|
|
1045
|
+
*/
|
|
1046
|
+
pause() {
|
|
1047
|
+
this._isPaused = true;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Resumes queue execution, immediately dispatching waiting tasks up to available concurrency.
|
|
1051
|
+
*/
|
|
1052
|
+
resume() {
|
|
1053
|
+
if (this._isPaused) {
|
|
1054
|
+
this._isPaused = false;
|
|
1055
|
+
this.pump();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Checks whether the task queue is currently paused.
|
|
1060
|
+
*/
|
|
1061
|
+
isPaused() {
|
|
1062
|
+
return this._isPaused;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Cancels all pending, delayed, and active tasks marked with the specified tag.
|
|
1066
|
+
*
|
|
1067
|
+
* @param tag - Tag identifier to match.
|
|
1068
|
+
* @param reason - Optional cancellation reason.
|
|
1069
|
+
* @returns Total count of tasks cancelled.
|
|
1070
|
+
*/
|
|
1071
|
+
cancelByTag(tag, reason) {
|
|
1072
|
+
const runners = this.tagIndex.get(tag);
|
|
1073
|
+
if (!runners || runners.size === 0) {
|
|
1074
|
+
return 0;
|
|
1075
|
+
}
|
|
1076
|
+
const list = Array.from(runners);
|
|
1077
|
+
let count = 0;
|
|
1078
|
+
for (const runner of list) {
|
|
1079
|
+
if (runner.state === "pending" /* PENDING */ || runner.state === "running" /* RUNNING */) {
|
|
1080
|
+
runner.cancel(reason ?? `Task cancelled by tag "${tag}"`);
|
|
1081
|
+
count++;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return count;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Returns active and pending task counts for a given tag.
|
|
1088
|
+
*
|
|
1089
|
+
* @param tag - Tag identifier.
|
|
1090
|
+
*/
|
|
1091
|
+
getStatsByTag(tag) {
|
|
1092
|
+
const runners = this.tagIndex.get(tag);
|
|
1093
|
+
if (!runners) {
|
|
1094
|
+
return { activeTasks: 0, pendingTasks: 0 };
|
|
1095
|
+
}
|
|
1096
|
+
let active = 0;
|
|
1097
|
+
let pending = 0;
|
|
1098
|
+
for (const runner of runners) {
|
|
1099
|
+
if (runner.state === "running" /* RUNNING */) {
|
|
1100
|
+
active++;
|
|
1101
|
+
} else if (runner.state === "pending" /* PENDING */) {
|
|
1102
|
+
pending++;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return { activeTasks: active, pendingTasks: pending };
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Indexes a runner under all its associated tags.
|
|
1109
|
+
*/
|
|
1110
|
+
indexTaskTags(runner) {
|
|
1111
|
+
for (const tag of runner.tags) {
|
|
1112
|
+
let set = this.tagIndex.get(tag);
|
|
1113
|
+
if (!set) {
|
|
1114
|
+
set = /* @__PURE__ */ new Set();
|
|
1115
|
+
this.tagIndex.set(tag, set);
|
|
1116
|
+
}
|
|
1117
|
+
set.add(runner);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Removes a runner from the tag index upon settlement.
|
|
1122
|
+
*/
|
|
1123
|
+
cleanupTaskTags(runner) {
|
|
1124
|
+
for (const tag of runner.tags) {
|
|
1125
|
+
const set = this.tagIndex.get(tag);
|
|
1126
|
+
if (set) {
|
|
1127
|
+
set.delete(runner);
|
|
1128
|
+
if (set.size === 0) {
|
|
1129
|
+
this.tagIndex.delete(tag);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Inserts a task runner into the queue based on priority weight (descending).
|
|
1136
|
+
* Preserves FIFO ordering among tasks with identical priority.
|
|
1137
|
+
*/
|
|
1138
|
+
insertIntoQueue(runner) {
|
|
1139
|
+
const options = this.runnerOptions.get(runner);
|
|
1140
|
+
const targetWeight = resolvePriorityWeight(options?.priority);
|
|
1141
|
+
let insertIndex = this.queue.length;
|
|
1142
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
1143
|
+
const existingOptions = this.runnerOptions.get(this.queue[i]);
|
|
1144
|
+
const existingWeight = resolvePriorityWeight(existingOptions?.priority);
|
|
1145
|
+
if (existingWeight < targetWeight) {
|
|
1146
|
+
insertIndex = i;
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
this.queue.splice(insertIndex, 0, runner);
|
|
1151
|
+
}
|
|
608
1152
|
/**
|
|
609
1153
|
* Enqueues a task runner according to the specified schedule options.
|
|
610
1154
|
*
|
|
@@ -658,13 +1202,35 @@ var TaskQueue = class {
|
|
|
658
1202
|
);
|
|
659
1203
|
}
|
|
660
1204
|
}
|
|
1205
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
1206
|
+
if (typeof options.totalTimeoutMs !== "number" || Number.isNaN(options.totalTimeoutMs) || !Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0) {
|
|
1207
|
+
throw new AhkoConfigurationError(
|
|
1208
|
+
`Invalid totalTimeoutMs "${options.totalTimeoutMs}". totalTimeoutMs must be a positive finite number greater than 0.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
661
1212
|
if (options) {
|
|
662
1213
|
this.runnerOptions.set(runner, options);
|
|
663
1214
|
}
|
|
1215
|
+
this.indexTaskTags(runner);
|
|
1216
|
+
runner.promise.finally(() => {
|
|
1217
|
+
this.cleanupTaskTags(runner);
|
|
1218
|
+
}).catch(() => {
|
|
1219
|
+
});
|
|
664
1220
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
665
1221
|
this.cancelledTasks++;
|
|
666
1222
|
return runner.promise;
|
|
667
1223
|
}
|
|
1224
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
1225
|
+
const budgetMs = options.totalTimeoutMs;
|
|
1226
|
+
const totalTimerId = setTimeout(() => {
|
|
1227
|
+
runner.timeout(budgetMs, `Task total execution deadline exceeded after ${budgetMs}ms`);
|
|
1228
|
+
}, budgetMs);
|
|
1229
|
+
runner.promise.finally(() => {
|
|
1230
|
+
clearTimeout(totalTimerId);
|
|
1231
|
+
}).catch(() => {
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
668
1234
|
if (strategy === "delay" /* DELAY */) {
|
|
669
1235
|
const delayMs = options?.delay ?? 0;
|
|
670
1236
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -688,15 +1254,23 @@ var TaskQueue = class {
|
|
|
688
1254
|
const index = this.queue.indexOf(runner);
|
|
689
1255
|
if (index !== -1) {
|
|
690
1256
|
this.queue.splice(index, 1);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1257
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1258
|
+
this.timedOutTasks++;
|
|
1259
|
+
this.emitter.emit("task:timeout", {
|
|
1260
|
+
taskId: runner.taskId,
|
|
1261
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1262
|
+
});
|
|
1263
|
+
} else {
|
|
1264
|
+
this.cancelledTasks++;
|
|
1265
|
+
this.emitter.emit("task:cancel", {
|
|
1266
|
+
taskId: runner.taskId,
|
|
1267
|
+
reason: "Task cancelled while queued"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
696
1270
|
this.checkIdle();
|
|
697
1271
|
}
|
|
698
1272
|
};
|
|
699
|
-
this.
|
|
1273
|
+
this.insertIntoQueue(runner);
|
|
700
1274
|
this.pump();
|
|
701
1275
|
return runner.promise;
|
|
702
1276
|
}
|
|
@@ -709,22 +1283,30 @@ var TaskQueue = class {
|
|
|
709
1283
|
runner,
|
|
710
1284
|
timerId: setTimeout(() => {
|
|
711
1285
|
this.delayedEntries.delete(delayedEntry);
|
|
712
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1286
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
713
1287
|
return;
|
|
714
1288
|
}
|
|
715
1289
|
runner.onCancel = () => {
|
|
716
1290
|
const index = this.queue.indexOf(runner);
|
|
717
1291
|
if (index !== -1) {
|
|
718
1292
|
this.queue.splice(index, 1);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1293
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1294
|
+
this.timedOutTasks++;
|
|
1295
|
+
this.emitter.emit("task:timeout", {
|
|
1296
|
+
taskId: runner.taskId,
|
|
1297
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1298
|
+
});
|
|
1299
|
+
} else {
|
|
1300
|
+
this.cancelledTasks++;
|
|
1301
|
+
this.emitter.emit("task:cancel", {
|
|
1302
|
+
taskId: runner.taskId,
|
|
1303
|
+
reason: "Task cancelled while queued"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
724
1306
|
this.checkIdle();
|
|
725
1307
|
}
|
|
726
1308
|
};
|
|
727
|
-
this.
|
|
1309
|
+
this.insertIntoQueue(runner);
|
|
728
1310
|
this.pump();
|
|
729
1311
|
}, delayMs)
|
|
730
1312
|
};
|
|
@@ -733,11 +1315,19 @@ var TaskQueue = class {
|
|
|
733
1315
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
734
1316
|
clearTimeout(delayedEntry.timerId);
|
|
735
1317
|
this.delayedEntries.delete(delayedEntry);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1318
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1319
|
+
this.timedOutTasks++;
|
|
1320
|
+
this.emitter.emit("task:timeout", {
|
|
1321
|
+
taskId: runner.taskId,
|
|
1322
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1323
|
+
});
|
|
1324
|
+
} else {
|
|
1325
|
+
this.cancelledTasks++;
|
|
1326
|
+
this.emitter.emit("task:cancel", {
|
|
1327
|
+
taskId: runner.taskId,
|
|
1328
|
+
reason: "Task cancelled while waiting in delay"
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
741
1331
|
this.checkIdle();
|
|
742
1332
|
}
|
|
743
1333
|
};
|
|
@@ -750,22 +1340,30 @@ var TaskQueue = class {
|
|
|
750
1340
|
let idleEntry;
|
|
751
1341
|
const handle = IdleScheduler.schedule(() => {
|
|
752
1342
|
this.idleEntries.delete(idleEntry);
|
|
753
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1343
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
754
1344
|
return;
|
|
755
1345
|
}
|
|
756
1346
|
runner.onCancel = () => {
|
|
757
1347
|
const index = this.queue.indexOf(runner);
|
|
758
1348
|
if (index !== -1) {
|
|
759
1349
|
this.queue.splice(index, 1);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1355
|
+
});
|
|
1356
|
+
} else {
|
|
1357
|
+
this.cancelledTasks++;
|
|
1358
|
+
this.emitter.emit("task:cancel", {
|
|
1359
|
+
taskId: runner.taskId,
|
|
1360
|
+
reason: "Task cancelled while queued"
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
765
1363
|
this.checkIdle();
|
|
766
1364
|
}
|
|
767
1365
|
};
|
|
768
|
-
this.
|
|
1366
|
+
this.insertIntoQueue(runner);
|
|
769
1367
|
this.pump();
|
|
770
1368
|
}, idleTimeout);
|
|
771
1369
|
idleEntry = { runner, handle };
|
|
@@ -774,21 +1372,30 @@ var TaskQueue = class {
|
|
|
774
1372
|
if (this.idleEntries.has(idleEntry)) {
|
|
775
1373
|
handle.cancel();
|
|
776
1374
|
this.idleEntries.delete(idleEntry);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1375
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1376
|
+
this.timedOutTasks++;
|
|
1377
|
+
this.emitter.emit("task:timeout", {
|
|
1378
|
+
taskId: runner.taskId,
|
|
1379
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1380
|
+
});
|
|
1381
|
+
} else {
|
|
1382
|
+
this.cancelledTasks++;
|
|
1383
|
+
this.emitter.emit("task:cancel", {
|
|
1384
|
+
taskId: runner.taskId,
|
|
1385
|
+
reason: "Task cancelled while waiting for idle"
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
782
1388
|
this.checkIdle();
|
|
783
1389
|
}
|
|
784
1390
|
};
|
|
785
1391
|
}
|
|
786
1392
|
/**
|
|
787
1393
|
* Pumps the queue by picking pending tasks and executing them
|
|
788
|
-
* as long as concurrency capacity is available
|
|
1394
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1395
|
+
* and queue is not paused.
|
|
789
1396
|
*/
|
|
790
1397
|
pump() {
|
|
791
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.
|
|
1398
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this._concurrency) {
|
|
792
1399
|
return;
|
|
793
1400
|
}
|
|
794
1401
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -805,7 +1412,7 @@ var TaskQueue = class {
|
|
|
805
1412
|
return;
|
|
806
1413
|
}
|
|
807
1414
|
}
|
|
808
|
-
while (this.activeRunners.size < this.
|
|
1415
|
+
while (!this._isPaused && this.activeRunners.size < this._concurrency && this.queue.length > 0) {
|
|
809
1416
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
810
1417
|
const now = Date.now();
|
|
811
1418
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -824,14 +1431,30 @@ var TaskQueue = class {
|
|
|
824
1431
|
if (!runner) {
|
|
825
1432
|
break;
|
|
826
1433
|
}
|
|
827
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1434
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
828
1435
|
continue;
|
|
829
1436
|
}
|
|
1437
|
+
if (this.circuitBreakerCoordinator) {
|
|
1438
|
+
try {
|
|
1439
|
+
this.circuitBreakerCoordinator.checkAllowed();
|
|
1440
|
+
} catch (cbError) {
|
|
1441
|
+
this.failedTasks++;
|
|
1442
|
+
this.runnerOptions.delete(runner);
|
|
1443
|
+
this.emitter.emit("task:fail", {
|
|
1444
|
+
taskId: runner.taskId,
|
|
1445
|
+
attempt: runner.attempt,
|
|
1446
|
+
error: cbError,
|
|
1447
|
+
willRetry: false
|
|
1448
|
+
});
|
|
1449
|
+
runner.reject(cbError);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
830
1453
|
this.activeRunners.add(runner);
|
|
831
1454
|
this.lastTaskStartTime = Date.now();
|
|
832
1455
|
void this.executeRunner(runner);
|
|
833
1456
|
if (this.minIntervalMs > 0) {
|
|
834
|
-
if (this.queue.length > 0 && this.activeRunners.size < this.
|
|
1457
|
+
if (this.queue.length > 0 && this.activeRunners.size < this._concurrency) {
|
|
835
1458
|
if (this.rateLimitTimer === void 0) {
|
|
836
1459
|
this.rateLimitTimer = setTimeout(() => {
|
|
837
1460
|
this.rateLimitTimer = void 0;
|
|
@@ -855,6 +1478,8 @@ var TaskQueue = class {
|
|
|
855
1478
|
});
|
|
856
1479
|
try {
|
|
857
1480
|
const result = await runner.run();
|
|
1481
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
1482
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
858
1483
|
this.completedTasks++;
|
|
859
1484
|
this.activeRunners.delete(runner);
|
|
860
1485
|
this.runnerOptions.delete(runner);
|
|
@@ -890,11 +1515,15 @@ var TaskQueue = class {
|
|
|
890
1515
|
this.scheduleRetry(runner, options);
|
|
891
1516
|
return;
|
|
892
1517
|
}
|
|
1518
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1519
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1520
|
+
}
|
|
1521
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
893
1522
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
894
1523
|
this.timedOutTasks++;
|
|
895
1524
|
this.emitter.emit("task:timeout", {
|
|
896
1525
|
taskId: runner.taskId,
|
|
897
|
-
timeoutMs: runner.timeoutMs
|
|
1526
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
898
1527
|
});
|
|
899
1528
|
} else {
|
|
900
1529
|
this.failedTasks++;
|
|
@@ -924,15 +1553,23 @@ var TaskQueue = class {
|
|
|
924
1553
|
const index = this.queue.indexOf(runner);
|
|
925
1554
|
if (index !== -1) {
|
|
926
1555
|
this.queue.splice(index, 1);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1556
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1557
|
+
this.timedOutTasks++;
|
|
1558
|
+
this.emitter.emit("task:timeout", {
|
|
1559
|
+
taskId: runner.taskId,
|
|
1560
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1561
|
+
});
|
|
1562
|
+
} else {
|
|
1563
|
+
this.cancelledTasks++;
|
|
1564
|
+
this.emitter.emit("task:cancel", {
|
|
1565
|
+
taskId: runner.taskId,
|
|
1566
|
+
reason: "Task cancelled while queued"
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
932
1569
|
this.checkIdle();
|
|
933
1570
|
}
|
|
934
1571
|
};
|
|
935
|
-
this.
|
|
1572
|
+
this.insertIntoQueue(runner);
|
|
936
1573
|
this.pump();
|
|
937
1574
|
return;
|
|
938
1575
|
}
|
|
@@ -940,22 +1577,30 @@ var TaskQueue = class {
|
|
|
940
1577
|
runner,
|
|
941
1578
|
timerId: setTimeout(() => {
|
|
942
1579
|
this.retryEntries.delete(retryEntry);
|
|
943
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1580
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
944
1581
|
return;
|
|
945
1582
|
}
|
|
946
1583
|
runner.onCancel = () => {
|
|
947
1584
|
const index = this.queue.indexOf(runner);
|
|
948
1585
|
if (index !== -1) {
|
|
949
1586
|
this.queue.splice(index, 1);
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1587
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1588
|
+
this.timedOutTasks++;
|
|
1589
|
+
this.emitter.emit("task:timeout", {
|
|
1590
|
+
taskId: runner.taskId,
|
|
1591
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1592
|
+
});
|
|
1593
|
+
} else {
|
|
1594
|
+
this.cancelledTasks++;
|
|
1595
|
+
this.emitter.emit("task:cancel", {
|
|
1596
|
+
taskId: runner.taskId,
|
|
1597
|
+
reason: "Task cancelled while queued"
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
955
1600
|
this.checkIdle();
|
|
956
1601
|
}
|
|
957
1602
|
};
|
|
958
|
-
this.
|
|
1603
|
+
this.insertIntoQueue(runner);
|
|
959
1604
|
this.pump();
|
|
960
1605
|
}, backoffDelay)
|
|
961
1606
|
};
|
|
@@ -964,11 +1609,19 @@ var TaskQueue = class {
|
|
|
964
1609
|
if (this.retryEntries.has(retryEntry)) {
|
|
965
1610
|
clearTimeout(retryEntry.timerId);
|
|
966
1611
|
this.retryEntries.delete(retryEntry);
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1612
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1613
|
+
this.timedOutTasks++;
|
|
1614
|
+
this.emitter.emit("task:timeout", {
|
|
1615
|
+
taskId: runner.taskId,
|
|
1616
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1617
|
+
});
|
|
1618
|
+
} else {
|
|
1619
|
+
this.cancelledTasks++;
|
|
1620
|
+
this.emitter.emit("task:cancel", {
|
|
1621
|
+
taskId: runner.taskId,
|
|
1622
|
+
reason: "Task cancelled during retry backoff"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
972
1625
|
this.checkIdle();
|
|
973
1626
|
}
|
|
974
1627
|
};
|
|
@@ -1015,7 +1668,7 @@ var TaskQueue = class {
|
|
|
1015
1668
|
clear() {
|
|
1016
1669
|
while (this.queue.length > 0) {
|
|
1017
1670
|
const runner = this.queue.shift();
|
|
1018
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1671
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
1019
1672
|
runner.cancel("Scheduler cleared");
|
|
1020
1673
|
this.cancelledTasks++;
|
|
1021
1674
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1065,7 +1718,10 @@ var TaskQueue = class {
|
|
|
1065
1718
|
timedOutTasks: this.timedOutTasks,
|
|
1066
1719
|
retriedTasks: this.retriedTasks,
|
|
1067
1720
|
totalDispatched: this.totalDispatched,
|
|
1068
|
-
capacity: this.
|
|
1721
|
+
capacity: this._concurrency,
|
|
1722
|
+
isPaused: this._isPaused,
|
|
1723
|
+
circuitState: this.circuitBreakerCoordinator?.state,
|
|
1724
|
+
adaptive: this.adaptiveCoordinator?.getStats()
|
|
1069
1725
|
});
|
|
1070
1726
|
}
|
|
1071
1727
|
};
|
|
@@ -1101,18 +1757,22 @@ var TaskRunner = class {
|
|
|
1101
1757
|
attempt = 1;
|
|
1102
1758
|
/** Duration of the most recent execution attempt in milliseconds */
|
|
1103
1759
|
lastDurationMs = 0;
|
|
1760
|
+
/** Set of classification tags associated with this task */
|
|
1761
|
+
tags;
|
|
1104
1762
|
/**
|
|
1105
1763
|
* Creates a new TaskRunner instance.
|
|
1106
1764
|
*
|
|
1107
1765
|
* @param task - The asynchronous work unit to run.
|
|
1108
1766
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
1109
1767
|
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
1768
|
+
* @param tags - Optional array of tags for classifying and selectively cancelling tasks.
|
|
1110
1769
|
*/
|
|
1111
|
-
constructor(task, externalSignal, timeoutMs) {
|
|
1770
|
+
constructor(task, externalSignal, timeoutMs, tags) {
|
|
1112
1771
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1113
1772
|
this.task = task;
|
|
1114
1773
|
this.externalSignal = externalSignal;
|
|
1115
1774
|
this.timeoutMs = timeoutMs;
|
|
1775
|
+
this.tags = new Set(tags ?? []);
|
|
1116
1776
|
this.abortController = new AbortController();
|
|
1117
1777
|
this.promise = new Promise((resolve, reject) => {
|
|
1118
1778
|
this.resolvePromise = resolve;
|
|
@@ -1136,6 +1796,8 @@ var TaskRunner = class {
|
|
|
1136
1796
|
}
|
|
1137
1797
|
}
|
|
1138
1798
|
}
|
|
1799
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1800
|
+
totalTimedOut = false;
|
|
1139
1801
|
/**
|
|
1140
1802
|
* Gets the current lifecycle state of the task.
|
|
1141
1803
|
*/
|
|
@@ -1168,7 +1830,7 @@ var TaskRunner = class {
|
|
|
1168
1830
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1169
1831
|
*/
|
|
1170
1832
|
async canRetry(error, retryOptions) {
|
|
1171
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1833
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1172
1834
|
return false;
|
|
1173
1835
|
}
|
|
1174
1836
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1209,15 +1871,16 @@ var TaskRunner = class {
|
|
|
1209
1871
|
let abortListener;
|
|
1210
1872
|
const abortPromise = new Promise((_, reject) => {
|
|
1211
1873
|
abortListener = () => {
|
|
1212
|
-
|
|
1874
|
+
const reason = this.abortController.signal.reason;
|
|
1875
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || reason instanceof AhkoTimeoutError) {
|
|
1876
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1213
1877
|
reject(
|
|
1214
|
-
new AhkoTimeoutError(
|
|
1878
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1215
1879
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1216
1880
|
{ timeoutMs: this.timeoutMs }
|
|
1217
1881
|
)
|
|
1218
1882
|
);
|
|
1219
1883
|
} else {
|
|
1220
|
-
const reason = this.abortController.signal.reason;
|
|
1221
1884
|
reject(
|
|
1222
1885
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1223
1886
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1252,6 +1915,10 @@ var TaskRunner = class {
|
|
|
1252
1915
|
}
|
|
1253
1916
|
taskExecutionPromise.catch(() => {
|
|
1254
1917
|
});
|
|
1918
|
+
abortPromise.catch(() => {
|
|
1919
|
+
});
|
|
1920
|
+
timeoutPromise?.catch(() => {
|
|
1921
|
+
});
|
|
1255
1922
|
const racePromises = [
|
|
1256
1923
|
taskExecutionPromise,
|
|
1257
1924
|
abortPromise
|
|
@@ -1343,6 +2010,31 @@ var TaskRunner = class {
|
|
|
1343
2010
|
this.onCancel?.(this);
|
|
1344
2011
|
}
|
|
1345
2012
|
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Times out the task, aborting pending or running execution with AhkoTimeoutError.
|
|
2015
|
+
*
|
|
2016
|
+
* @param timeoutMs - Timeout duration in milliseconds.
|
|
2017
|
+
* @param message - Optional custom timeout message.
|
|
2018
|
+
*/
|
|
2019
|
+
timeout(timeoutMs, message) {
|
|
2020
|
+
if (this._state === "completed" /* COMPLETED */ || this._state === "failed" /* FAILED */ || this._state === "cancelled" /* CANCELLED */ || this._state === "timed_out" /* TIMED_OUT */) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const wasPending = this._state === "pending" /* PENDING */;
|
|
2024
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
2025
|
+
this.totalTimedOut = true;
|
|
2026
|
+
this.clearTimeoutTimer();
|
|
2027
|
+
const timeoutError = new AhkoTimeoutError(
|
|
2028
|
+
message ?? `Task execution timed out after ${timeoutMs}ms`,
|
|
2029
|
+
{ timeoutMs }
|
|
2030
|
+
);
|
|
2031
|
+
this.abortController.abort(timeoutError);
|
|
2032
|
+
this.cleanup();
|
|
2033
|
+
if (wasPending) {
|
|
2034
|
+
this.rejectPromise(timeoutError);
|
|
2035
|
+
this.onCancel?.(this);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1346
2038
|
/**
|
|
1347
2039
|
* Handles external AbortSignal trigger.
|
|
1348
2040
|
*/
|
|
@@ -1361,9 +2053,56 @@ var TaskRunner = class {
|
|
|
1361
2053
|
};
|
|
1362
2054
|
|
|
1363
2055
|
// src/ahko.ts
|
|
1364
|
-
var Ahko = class {
|
|
2056
|
+
var Ahko = class _Ahko {
|
|
1365
2057
|
/** Internal queue and concurrency manager */
|
|
1366
2058
|
queue;
|
|
2059
|
+
/** Default schedule options inherited from profile if configured */
|
|
2060
|
+
defaultScheduleOptions;
|
|
2061
|
+
/**
|
|
2062
|
+
* Programmatically loads a declarative configuration into memory.
|
|
2063
|
+
* Works universally across Node.js, browsers, and edge runtimes.
|
|
2064
|
+
*
|
|
2065
|
+
* @param config - File configuration object containing default and named profiles.
|
|
2066
|
+
*/
|
|
2067
|
+
static loadConfig(config) {
|
|
2068
|
+
loadConfig(config);
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Asynchronously loads a configuration file from disk (Node.js).
|
|
2072
|
+
*
|
|
2073
|
+
* @param filePath - Path to configuration file (default: "config.ahko.json").
|
|
2074
|
+
*/
|
|
2075
|
+
static async loadConfigFile(filePath) {
|
|
2076
|
+
return loadConfigFile(filePath);
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Resets the active declarative configuration.
|
|
2080
|
+
*/
|
|
2081
|
+
static resetConfig() {
|
|
2082
|
+
resetConfig();
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Retrieves the currently active declarative configuration.
|
|
2086
|
+
*/
|
|
2087
|
+
static getActiveConfig() {
|
|
2088
|
+
return getActiveConfig();
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Instantiates an Ahko scheduler initialized with settings from a declarative profile.
|
|
2092
|
+
*
|
|
2093
|
+
* @param profileName - Optional name of the profile (e.g. "api", "background").
|
|
2094
|
+
* @param overrides - Optional scheduler options overriding profile values.
|
|
2095
|
+
* @returns A new configured Ahko instance.
|
|
2096
|
+
*/
|
|
2097
|
+
static fromProfile(profileName, overrides) {
|
|
2098
|
+
const profile = getProfileConfig(profileName);
|
|
2099
|
+
return new _Ahko({
|
|
2100
|
+
...profile,
|
|
2101
|
+
...overrides,
|
|
2102
|
+
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2103
|
+
adaptive: overrides?.adaptive ?? profile?.adaptive
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
1367
2106
|
/**
|
|
1368
2107
|
* Initializes a new Ahko scheduler instance.
|
|
1369
2108
|
*
|
|
@@ -1376,79 +2115,177 @@ var Ahko = class {
|
|
|
1376
2115
|
* ```
|
|
1377
2116
|
*/
|
|
1378
2117
|
constructor(options) {
|
|
1379
|
-
|
|
2118
|
+
const profile = options?.profile ? getProfileConfig(options.profile) : getProfileConfig();
|
|
2119
|
+
const mergedOptions = {
|
|
2120
|
+
...profile,
|
|
2121
|
+
...options,
|
|
2122
|
+
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2123
|
+
adaptive: options?.adaptive ?? profile?.adaptive
|
|
2124
|
+
};
|
|
2125
|
+
if (profile) {
|
|
2126
|
+
this.defaultScheduleOptions = {
|
|
2127
|
+
priority: profile.priority,
|
|
2128
|
+
retry: profile.retry,
|
|
2129
|
+
timeoutMs: profile.timeoutMs,
|
|
2130
|
+
totalTimeoutMs: profile.totalTimeoutMs,
|
|
2131
|
+
tags: profile.tags
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
this.queue = new TaskQueue(
|
|
2135
|
+
mergedOptions.concurrency,
|
|
2136
|
+
mergedOptions.minIntervalMs,
|
|
2137
|
+
mergedOptions.circuitBreaker,
|
|
2138
|
+
mergedOptions.adaptive
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Current concurrency limit.
|
|
2143
|
+
*/
|
|
2144
|
+
get concurrency() {
|
|
2145
|
+
return this.queue.concurrency;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Dynamically updates the concurrency limit of the scheduler.
|
|
2149
|
+
*
|
|
2150
|
+
* @param concurrency - New maximum concurrency (must be >= 1).
|
|
2151
|
+
* @throws {AhkoConfigurationError} If concurrency is invalid.
|
|
2152
|
+
*/
|
|
2153
|
+
setConcurrency(concurrency) {
|
|
2154
|
+
this.queue.setConcurrency(concurrency);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
|
|
2158
|
+
*/
|
|
2159
|
+
pause() {
|
|
2160
|
+
this.queue.pause();
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Resumes scheduler dispatch, immediately executing waiting tasks up to available concurrency.
|
|
2164
|
+
*/
|
|
2165
|
+
resume() {
|
|
2166
|
+
this.queue.resume();
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Checks whether the scheduler is currently paused.
|
|
2170
|
+
*/
|
|
2171
|
+
isPaused() {
|
|
2172
|
+
return this.queue.isPaused();
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Current circuit breaker state if circuit breaker protection is configured.
|
|
2176
|
+
*/
|
|
2177
|
+
get circuitState() {
|
|
2178
|
+
return this.queue.circuitBreakerCoordinator?.state;
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Access to the underlying circuit breaker coordinator instance if configured.
|
|
2182
|
+
*/
|
|
2183
|
+
get circuitBreaker() {
|
|
2184
|
+
return this.queue.circuitBreakerCoordinator;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Wraps an async function so every execution is automatically routed through this Ahko scheduler.
|
|
2188
|
+
*
|
|
2189
|
+
* @template TArgs - Parameter types of the wrapped function.
|
|
2190
|
+
* @template TReturn - Return type of the wrapped function.
|
|
2191
|
+
* @param fn - The function to wrap.
|
|
2192
|
+
* @param options - Optional scheduling options applied to every wrapped call.
|
|
2193
|
+
* @returns A wrapped function returning a Promise.
|
|
2194
|
+
*
|
|
2195
|
+
* @example
|
|
2196
|
+
* ```typescript
|
|
2197
|
+
* const fetchUser = ahko.wrap(async (id: string) => api.getUser(id), { priority: "high" });
|
|
2198
|
+
* const user = await fetchUser("usr_123");
|
|
2199
|
+
* ```
|
|
2200
|
+
*/
|
|
2201
|
+
wrap(fn, options) {
|
|
2202
|
+
if (typeof fn !== "function") {
|
|
2203
|
+
throw new AhkoConfigurationError("Target to wrap must be a valid function.");
|
|
2204
|
+
}
|
|
2205
|
+
return (...args) => {
|
|
2206
|
+
return this.schedule(() => fn(...args), options);
|
|
2207
|
+
};
|
|
1380
2208
|
}
|
|
1381
2209
|
/**
|
|
1382
2210
|
* Schedules a task for execution with full return type inference.
|
|
1383
2211
|
*
|
|
1384
2212
|
* @template T - Inferred return type of the task.
|
|
1385
2213
|
* @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.
|
|
2214
|
+
* @param options - Task-specific scheduling options such as strategy, priority, delay, and cancellation signal.
|
|
1387
2215
|
* @returns A promise that resolves with the task's return value.
|
|
1388
2216
|
*
|
|
1389
2217
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1390
2218
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1391
|
-
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
2219
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs or totalTimeoutMs.
|
|
2220
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit breaker is OPEN and rejects the execution.
|
|
1392
2221
|
*
|
|
1393
2222
|
* @example
|
|
1394
2223
|
* ```typescript
|
|
1395
2224
|
* // Immediate execution (subject to concurrency)
|
|
1396
2225
|
* const count = await ahko.schedule(async () => 42);
|
|
1397
2226
|
*
|
|
1398
|
-
* //
|
|
1399
|
-
* await ahko.schedule(
|
|
1400
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1401
|
-
* { strategy: "delay", delay: 1000 }
|
|
1402
|
-
* );
|
|
2227
|
+
* // High priority task
|
|
2228
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1403
2229
|
* ```
|
|
1404
2230
|
*/
|
|
1405
2231
|
schedule(task, options) {
|
|
1406
2232
|
if (typeof task !== "function") {
|
|
1407
2233
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1408
2234
|
}
|
|
1409
|
-
const
|
|
2235
|
+
const mergedTags = options?.tags ?? this.defaultScheduleOptions?.tags;
|
|
2236
|
+
const mergedOptions = {
|
|
2237
|
+
...this.defaultScheduleOptions,
|
|
2238
|
+
...options,
|
|
2239
|
+
tags: mergedTags
|
|
2240
|
+
};
|
|
2241
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1410
2242
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1411
|
-
if (!
|
|
2243
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1412
2244
|
throw new AhkoConfigurationError(
|
|
1413
2245
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1414
2246
|
);
|
|
1415
2247
|
}
|
|
1416
|
-
const waitMs =
|
|
2248
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1417
2249
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1418
2250
|
throw new AhkoConfigurationError(
|
|
1419
2251
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1420
2252
|
);
|
|
1421
2253
|
}
|
|
1422
2254
|
return this.queue.debounceCoordinator.schedule(
|
|
1423
|
-
|
|
2255
|
+
mergedOptions.key,
|
|
1424
2256
|
task,
|
|
1425
2257
|
waitMs,
|
|
1426
|
-
|
|
2258
|
+
mergedOptions,
|
|
1427
2259
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1428
2260
|
);
|
|
1429
2261
|
}
|
|
1430
2262
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1431
|
-
if (!
|
|
2263
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1432
2264
|
throw new AhkoConfigurationError(
|
|
1433
2265
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1434
2266
|
);
|
|
1435
2267
|
}
|
|
1436
|
-
const waitMs =
|
|
2268
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1437
2269
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1438
2270
|
throw new AhkoConfigurationError(
|
|
1439
2271
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1440
2272
|
);
|
|
1441
2273
|
}
|
|
1442
2274
|
return this.queue.throttleCoordinator.schedule(
|
|
1443
|
-
|
|
2275
|
+
mergedOptions.key,
|
|
1444
2276
|
task,
|
|
1445
2277
|
waitMs,
|
|
1446
|
-
|
|
2278
|
+
mergedOptions,
|
|
1447
2279
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1448
2280
|
);
|
|
1449
2281
|
}
|
|
1450
|
-
const runner = new TaskRunner(
|
|
1451
|
-
|
|
2282
|
+
const runner = new TaskRunner(
|
|
2283
|
+
task,
|
|
2284
|
+
mergedOptions.signal,
|
|
2285
|
+
mergedOptions.timeoutMs,
|
|
2286
|
+
mergedOptions.tags
|
|
2287
|
+
);
|
|
2288
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1452
2289
|
}
|
|
1453
2290
|
/**
|
|
1454
2291
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1508,15 +2345,185 @@ var Ahko = class {
|
|
|
1508
2345
|
waitMs
|
|
1509
2346
|
});
|
|
1510
2347
|
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Transforms an iterable of items concurrently using an asynchronous mapping function.
|
|
2350
|
+
*
|
|
2351
|
+
* Results are guaranteed to be returned in the original index order.
|
|
2352
|
+
* Concurrency can be capped per-batch or fall back to the scheduler's global limit.
|
|
2353
|
+
*
|
|
2354
|
+
* @template TItem - Type of input elements.
|
|
2355
|
+
* @template TResult - Type of mapped elements.
|
|
2356
|
+
* @param items - Iterable sequence of items to process.
|
|
2357
|
+
* @param fn - Mapper callback receiving item, index, and task context.
|
|
2358
|
+
* @param options - Batch execution options (concurrency, stopOnError, retry, signal, tags, etc.).
|
|
2359
|
+
* @returns Array of transformed results in index order.
|
|
2360
|
+
*
|
|
2361
|
+
* @throws {AhkoConfigurationError} If fn is not a function or concurrency is invalid.
|
|
2362
|
+
* @throws {AhkoCancellationError} If batch or item is cancelled.
|
|
2363
|
+
*
|
|
2364
|
+
* @example
|
|
2365
|
+
* ```typescript
|
|
2366
|
+
* const urls = ["/api/1", "/api/2", "/api/3"];
|
|
2367
|
+
* const data = await ahko.map(urls, async (url, i, { signal }) => {
|
|
2368
|
+
* const res = await fetch(url, { signal });
|
|
2369
|
+
* return res.json();
|
|
2370
|
+
* }, { concurrency: 2 });
|
|
2371
|
+
* ```
|
|
2372
|
+
*/
|
|
2373
|
+
async map(items, fn, options) {
|
|
2374
|
+
if (typeof fn !== "function") {
|
|
2375
|
+
throw new AhkoConfigurationError("Mapper function must be a valid function.");
|
|
2376
|
+
}
|
|
2377
|
+
if (options?.concurrency !== void 0 && (typeof options.concurrency !== "number" || Number.isNaN(options.concurrency) || options.concurrency < 1)) {
|
|
2378
|
+
throw new AhkoConfigurationError(
|
|
2379
|
+
`Invalid concurrency "${options.concurrency}". Must be a number greater than or equal to 1.`
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
const list = Array.from(items);
|
|
2383
|
+
if (list.length === 0) {
|
|
2384
|
+
return [];
|
|
2385
|
+
}
|
|
2386
|
+
const { concurrency, stopOnError = false, signal: externalSignal, ...scheduleOpts } = options ?? {};
|
|
2387
|
+
if (externalSignal?.aborted) {
|
|
2388
|
+
throw new AhkoCancellationError(
|
|
2389
|
+
externalSignal.reason ? `Batch cancelled: ${String(externalSignal.reason)}` : "Batch cancelled"
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
const abortController = new AbortController();
|
|
2393
|
+
const results = new Array(list.length);
|
|
2394
|
+
let firstError = void 0;
|
|
2395
|
+
let hasAborted = false;
|
|
2396
|
+
const localizedLimit = concurrency !== void 0 ? Math.floor(concurrency) : Number.isFinite(this.concurrency) ? this.concurrency : Infinity;
|
|
2397
|
+
return new Promise((resolve, reject) => {
|
|
2398
|
+
let currentIndex = 0;
|
|
2399
|
+
let activeCount = 0;
|
|
2400
|
+
let settledCount = 0;
|
|
2401
|
+
const onExternalAbort = () => {
|
|
2402
|
+
const reason = externalSignal?.reason ?? "Batch cancelled by external signal";
|
|
2403
|
+
const err = new AhkoCancellationError(
|
|
2404
|
+
typeof reason === "string" ? reason : "Batch cancelled by external signal"
|
|
2405
|
+
);
|
|
2406
|
+
cleanupAndReject(err);
|
|
2407
|
+
};
|
|
2408
|
+
if (externalSignal) {
|
|
2409
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
2410
|
+
}
|
|
2411
|
+
const cleanupAndReject = (err) => {
|
|
2412
|
+
if (!hasAborted) {
|
|
2413
|
+
hasAborted = true;
|
|
2414
|
+
abortController.abort(err);
|
|
2415
|
+
}
|
|
2416
|
+
if (externalSignal) {
|
|
2417
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2418
|
+
}
|
|
2419
|
+
reject(err);
|
|
2420
|
+
};
|
|
2421
|
+
const checkCompletion = () => {
|
|
2422
|
+
if (settledCount === list.length) {
|
|
2423
|
+
if (externalSignal) {
|
|
2424
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2425
|
+
}
|
|
2426
|
+
if (firstError !== void 0) {
|
|
2427
|
+
reject(firstError);
|
|
2428
|
+
} else {
|
|
2429
|
+
resolve(results);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
const launchNext = () => {
|
|
2434
|
+
if (hasAborted && stopOnError) {
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
while (currentIndex < list.length && activeCount < localizedLimit && !(hasAborted && stopOnError)) {
|
|
2438
|
+
const index = currentIndex++;
|
|
2439
|
+
const item = list[index];
|
|
2440
|
+
activeCount++;
|
|
2441
|
+
const taskPromise = this.schedule(
|
|
2442
|
+
(context) => fn(item, index, context),
|
|
2443
|
+
{
|
|
2444
|
+
...scheduleOpts,
|
|
2445
|
+
signal: abortController.signal
|
|
2446
|
+
}
|
|
2447
|
+
);
|
|
2448
|
+
taskPromise.then((result) => {
|
|
2449
|
+
results[index] = result;
|
|
2450
|
+
}).catch((err) => {
|
|
2451
|
+
if (firstError === void 0) {
|
|
2452
|
+
firstError = err;
|
|
2453
|
+
}
|
|
2454
|
+
if (stopOnError && !hasAborted) {
|
|
2455
|
+
cleanupAndReject(err);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
}).finally(() => {
|
|
2459
|
+
activeCount--;
|
|
2460
|
+
settledCount++;
|
|
2461
|
+
if (hasAborted && stopOnError) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (currentIndex < list.length) {
|
|
2465
|
+
launchNext();
|
|
2466
|
+
} else {
|
|
2467
|
+
checkCompletion();
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
if (abortController.signal.aborted) {
|
|
2473
|
+
cleanupAndReject(abortController.signal.reason);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
launchNext();
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Iterates sequentially or concurrently over an iterable sequence of items,
|
|
2481
|
+
* executing the callback function for each element.
|
|
2482
|
+
*
|
|
2483
|
+
* @template TItem - Type of input elements.
|
|
2484
|
+
* @param items - Iterable sequence of items to process.
|
|
2485
|
+
* @param fn - Callback receiving item, index, and task context.
|
|
2486
|
+
* @param options - Batch execution options.
|
|
2487
|
+
* @returns Promise resolving once all items have finished executing.
|
|
2488
|
+
*
|
|
2489
|
+
* @example
|
|
2490
|
+
* ```typescript
|
|
2491
|
+
* await ahko.each(userQueue, async (user, index, { signal }) => {
|
|
2492
|
+
* await sendWelcomeEmail(user, { signal });
|
|
2493
|
+
* }, { concurrency: 5 });
|
|
2494
|
+
* ```
|
|
2495
|
+
*/
|
|
2496
|
+
async each(items, fn, options) {
|
|
2497
|
+
await this.map(items, fn, options);
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Cancels all pending, delayed, and active tasks tagged with the given tag.
|
|
2501
|
+
*
|
|
2502
|
+
* @param tag - Tag identifier.
|
|
2503
|
+
* @param reason - Optional cancellation reason.
|
|
2504
|
+
* @returns Total number of tasks cancelled.
|
|
2505
|
+
*/
|
|
2506
|
+
cancelByTag(tag, reason) {
|
|
2507
|
+
return this.queue.cancelByTag(tag, reason);
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Retrieves active and pending task counts for a given tag.
|
|
2511
|
+
*
|
|
2512
|
+
* @param tag - Tag identifier.
|
|
2513
|
+
* @returns Object with activeTasks and pendingTasks counts.
|
|
2514
|
+
*/
|
|
2515
|
+
statsByTag(tag) {
|
|
2516
|
+
return this.queue.getStatsByTag(tag);
|
|
2517
|
+
}
|
|
1511
2518
|
/**
|
|
1512
2519
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1513
2520
|
*
|
|
1514
|
-
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled,
|
|
2521
|
+
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, timed out tasks, pause status, and circuit state.
|
|
1515
2522
|
*
|
|
1516
2523
|
* @example
|
|
1517
2524
|
* ```typescript
|
|
1518
2525
|
* const stats = ahko.stats();
|
|
1519
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2526
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1520
2527
|
* ```
|
|
1521
2528
|
*/
|
|
1522
2529
|
stats() {
|
|
@@ -1602,7 +2609,7 @@ var Ahko = class {
|
|
|
1602
2609
|
};
|
|
1603
2610
|
|
|
1604
2611
|
// src/version.ts
|
|
1605
|
-
var VERSION = "1.
|
|
2612
|
+
var VERSION = "1.1.5";
|
|
1606
2613
|
|
|
1607
2614
|
// src/errors/queue.error.ts
|
|
1608
2615
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1675,18 +2682,29 @@ function combineSignals(signals) {
|
|
|
1675
2682
|
}
|
|
1676
2683
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1677
2684
|
0 && (module.exports = {
|
|
2685
|
+
AdaptiveCoordinator,
|
|
1678
2686
|
Ahko,
|
|
1679
2687
|
AhkoCancellationError,
|
|
2688
|
+
AhkoCircuitBreakerOpenError,
|
|
1680
2689
|
AhkoConfigurationError,
|
|
1681
2690
|
AhkoError,
|
|
1682
2691
|
AhkoQueueError,
|
|
1683
2692
|
AhkoTimeoutError,
|
|
2693
|
+
CircuitBreakerCoordinator,
|
|
1684
2694
|
DEFAULT_BASE_DELAY,
|
|
1685
2695
|
DEFAULT_MAX_DELAY,
|
|
2696
|
+
ECircuitState,
|
|
1686
2697
|
EScheduleStrategy,
|
|
1687
2698
|
ETaskState,
|
|
2699
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1688
2700
|
VERSION,
|
|
1689
2701
|
calculateBackoff,
|
|
1690
|
-
combineSignals
|
|
2702
|
+
combineSignals,
|
|
2703
|
+
getActiveConfig,
|
|
2704
|
+
getProfileConfig,
|
|
2705
|
+
loadConfig,
|
|
2706
|
+
loadConfigFile,
|
|
2707
|
+
resetConfig,
|
|
2708
|
+
resolvePriorityWeight
|
|
1691
2709
|
});
|
|
1692
2710
|
//# sourceMappingURL=index.cjs.map
|