@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.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/errors/ahko.error.ts
|
|
2
9
|
var AhkoError = class extends Error {
|
|
3
10
|
/**
|
|
@@ -13,6 +20,21 @@ var AhkoError = class extends Error {
|
|
|
13
20
|
}
|
|
14
21
|
};
|
|
15
22
|
|
|
23
|
+
// src/errors/cancellation.error.ts
|
|
24
|
+
var AhkoCancellationError = class extends AhkoError {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new AhkoCancellationError.
|
|
27
|
+
*
|
|
28
|
+
* @param message - Reason for cancellation.
|
|
29
|
+
* @param options - Standard Error options including cause.
|
|
30
|
+
*/
|
|
31
|
+
constructor(message = "Task was cancelled", options) {
|
|
32
|
+
super(message, options);
|
|
33
|
+
this.name = "AhkoCancellationError";
|
|
34
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
16
38
|
// src/errors/configuration.error.ts
|
|
17
39
|
var AhkoConfigurationError = class extends AhkoError {
|
|
18
40
|
/**
|
|
@@ -28,6 +50,72 @@ var AhkoConfigurationError = class extends AhkoError {
|
|
|
28
50
|
}
|
|
29
51
|
};
|
|
30
52
|
|
|
53
|
+
// src/config/config-loader.ts
|
|
54
|
+
var activeConfig;
|
|
55
|
+
function loadConfig(config) {
|
|
56
|
+
activeConfig = { ...config };
|
|
57
|
+
}
|
|
58
|
+
function resetConfig() {
|
|
59
|
+
activeConfig = void 0;
|
|
60
|
+
}
|
|
61
|
+
async function loadConfigFile(filePath = "config.ahko.json") {
|
|
62
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { readFile } = await import("fs/promises");
|
|
67
|
+
const { resolve } = await import("path");
|
|
68
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
69
|
+
const content = await readFile(resolvedPath, "utf-8");
|
|
70
|
+
const parsed = JSON.parse(content);
|
|
71
|
+
activeConfig = parsed;
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function tryAutoDiscoverSync() {
|
|
78
|
+
if (activeConfig !== void 0 || typeof process === "undefined" || !process.versions?.node) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
let fs = null;
|
|
83
|
+
let path = null;
|
|
84
|
+
if (typeof process.getBuiltinModule === "function") {
|
|
85
|
+
const getBuiltin = process.getBuiltinModule;
|
|
86
|
+
fs = getBuiltin("node:fs");
|
|
87
|
+
path = getBuiltin("node:path");
|
|
88
|
+
} else if (typeof __require === "function") {
|
|
89
|
+
fs = __require("fs");
|
|
90
|
+
path = __require("path");
|
|
91
|
+
}
|
|
92
|
+
if (fs && path) {
|
|
93
|
+
const configPath = path.resolve(process.cwd(), "config.ahko.json");
|
|
94
|
+
if (fs.existsSync(configPath)) {
|
|
95
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
96
|
+
activeConfig = JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getActiveConfig() {
|
|
103
|
+
if (activeConfig === void 0) {
|
|
104
|
+
tryAutoDiscoverSync();
|
|
105
|
+
}
|
|
106
|
+
return activeConfig;
|
|
107
|
+
}
|
|
108
|
+
function getProfileConfig(profileName) {
|
|
109
|
+
const config = getActiveConfig();
|
|
110
|
+
if (!config) {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
if (profileName) {
|
|
114
|
+
return config.profiles?.[profileName];
|
|
115
|
+
}
|
|
116
|
+
return config.default;
|
|
117
|
+
}
|
|
118
|
+
|
|
31
119
|
// src/models/strategy.model.ts
|
|
32
120
|
var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
33
121
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
@@ -38,6 +126,23 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
38
126
|
return EScheduleStrategy2;
|
|
39
127
|
})(EScheduleStrategy || {});
|
|
40
128
|
|
|
129
|
+
// src/errors/circuit-breaker.error.ts
|
|
130
|
+
var AhkoCircuitBreakerOpenError = class extends AhkoError {
|
|
131
|
+
/** Time remaining in milliseconds before trial execution is allowed */
|
|
132
|
+
resetTimeoutMs;
|
|
133
|
+
/** Timestamp when the circuit tripped open */
|
|
134
|
+
trippedAt;
|
|
135
|
+
/** Total consecutive failures that caused the trip */
|
|
136
|
+
consecutiveFailures;
|
|
137
|
+
constructor(message = "Circuit breaker is open. Fast-failing task execution to protect downstream resources.", options) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.name = "AhkoCircuitBreakerOpenError";
|
|
140
|
+
this.resetTimeoutMs = options?.resetTimeoutMs;
|
|
141
|
+
this.trippedAt = options?.trippedAt;
|
|
142
|
+
this.consecutiveFailures = options?.consecutiveFailures;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
41
146
|
// src/errors/timeout.error.ts
|
|
42
147
|
var AhkoTimeoutError = class extends AhkoError {
|
|
43
148
|
/**
|
|
@@ -58,6 +163,28 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
58
163
|
}
|
|
59
164
|
};
|
|
60
165
|
|
|
166
|
+
// src/models/priority.model.ts
|
|
167
|
+
var TASK_PRIORITY_WEIGHTS = {
|
|
168
|
+
high: 10,
|
|
169
|
+
normal: 0,
|
|
170
|
+
low: -10
|
|
171
|
+
};
|
|
172
|
+
function resolvePriorityWeight(priority) {
|
|
173
|
+
if (priority === void 0) {
|
|
174
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
175
|
+
}
|
|
176
|
+
if (typeof priority === "number") {
|
|
177
|
+
return Number.isFinite(priority) ? priority : TASK_PRIORITY_WEIGHTS.normal;
|
|
178
|
+
}
|
|
179
|
+
if (priority === "high") {
|
|
180
|
+
return TASK_PRIORITY_WEIGHTS.high;
|
|
181
|
+
}
|
|
182
|
+
if (priority === "low") {
|
|
183
|
+
return TASK_PRIORITY_WEIGHTS.low;
|
|
184
|
+
}
|
|
185
|
+
return TASK_PRIORITY_WEIGHTS.normal;
|
|
186
|
+
}
|
|
187
|
+
|
|
61
188
|
// src/models/state.model.ts
|
|
62
189
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
63
190
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -94,18 +221,251 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
94
221
|
return Math.floor(cappedDelay);
|
|
95
222
|
}
|
|
96
223
|
|
|
97
|
-
// src/
|
|
98
|
-
var
|
|
224
|
+
// src/scheduler/adaptive-coordinator.ts
|
|
225
|
+
var AdaptiveCoordinator = class {
|
|
226
|
+
_currentConcurrency;
|
|
227
|
+
minConcurrency;
|
|
228
|
+
maxConcurrency;
|
|
229
|
+
targetLatencyMs;
|
|
230
|
+
sampleWindowSize;
|
|
231
|
+
backoffFactor;
|
|
232
|
+
recentDurations = [];
|
|
233
|
+
lastAverageLatencyMs = 0;
|
|
234
|
+
onConcurrencyChange;
|
|
99
235
|
/**
|
|
100
|
-
*
|
|
236
|
+
* Initializes a new AdaptiveCoordinator instance.
|
|
101
237
|
*
|
|
102
|
-
* @param
|
|
103
|
-
* @param
|
|
238
|
+
* @param options - Adaptive concurrency configuration options.
|
|
239
|
+
* @param initialConcurrency - Starting scheduler concurrency limit.
|
|
240
|
+
* @param onConcurrencyChange - Callback invoked when concurrency changes.
|
|
241
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
104
242
|
*/
|
|
105
|
-
constructor(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
243
|
+
constructor(options, initialConcurrency, onConcurrencyChange) {
|
|
244
|
+
if (typeof options.targetLatencyMs !== "number" || Number.isNaN(options.targetLatencyMs) || !Number.isFinite(options.targetLatencyMs) || options.targetLatencyMs <= 0) {
|
|
245
|
+
throw new AhkoConfigurationError(
|
|
246
|
+
`Invalid targetLatencyMs "${options.targetLatencyMs}". targetLatencyMs must be a positive number greater than 0.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const min = options.minConcurrency ?? 1;
|
|
250
|
+
if (typeof min !== "number" || Number.isNaN(min) || min < 1 || !Number.isInteger(min)) {
|
|
251
|
+
throw new AhkoConfigurationError(
|
|
252
|
+
`Invalid minConcurrency "${min}". minConcurrency must be an integer greater than or equal to 1.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const defaultMax = Number.isFinite(initialConcurrency) ? Math.max(min, initialConcurrency * 2) : Math.max(min, 10);
|
|
256
|
+
const max = options.maxConcurrency ?? defaultMax;
|
|
257
|
+
if (typeof max !== "number" || Number.isNaN(max) || max < min || !Number.isInteger(max)) {
|
|
258
|
+
throw new AhkoConfigurationError(
|
|
259
|
+
`Invalid maxConcurrency "${max}". maxConcurrency must be an integer greater than or equal to minConcurrency (${min}).`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const windowSize = options.sampleWindowSize ?? 5;
|
|
263
|
+
if (typeof windowSize !== "number" || Number.isNaN(windowSize) || windowSize < 1 || !Number.isInteger(windowSize)) {
|
|
264
|
+
throw new AhkoConfigurationError(
|
|
265
|
+
`Invalid sampleWindowSize "${windowSize}". sampleWindowSize must be an integer greater than or equal to 1.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const factor = options.backoffFactor ?? 0.7;
|
|
269
|
+
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0.1 || factor >= 0.99) {
|
|
270
|
+
throw new AhkoConfigurationError(
|
|
271
|
+
`Invalid backoffFactor "${factor}". backoffFactor must be a number between 0.1 and 0.99.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
this.minConcurrency = min;
|
|
275
|
+
this.maxConcurrency = max;
|
|
276
|
+
this.targetLatencyMs = options.targetLatencyMs;
|
|
277
|
+
this.sampleWindowSize = windowSize;
|
|
278
|
+
this.backoffFactor = factor;
|
|
279
|
+
this.onConcurrencyChange = onConcurrencyChange;
|
|
280
|
+
const clampedInitial = Number.isFinite(initialConcurrency) ? Math.min(Math.max(initialConcurrency, min), max) : min;
|
|
281
|
+
this._currentConcurrency = clampedInitial;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Current effective concurrency limit dictated by the adaptive controller.
|
|
285
|
+
*/
|
|
286
|
+
get currentConcurrency() {
|
|
287
|
+
return this._currentConcurrency;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Manually overrides the current concurrency within [minConcurrency, maxConcurrency].
|
|
291
|
+
*
|
|
292
|
+
* @param concurrency - New concurrency limit to set.
|
|
293
|
+
*/
|
|
294
|
+
setConcurrency(concurrency) {
|
|
295
|
+
const clamped = Math.min(Math.max(concurrency, this.minConcurrency), this.maxConcurrency);
|
|
296
|
+
if (clamped !== this._currentConcurrency) {
|
|
297
|
+
const prev = this._currentConcurrency;
|
|
298
|
+
this._currentConcurrency = clamped;
|
|
299
|
+
this.onConcurrencyChange(prev, clamped, "Manual concurrency override");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Records a task execution duration sample and triggers AIMD adjustment if window is filled.
|
|
304
|
+
*
|
|
305
|
+
* @param durationMs - Execution duration in milliseconds of the completed task.
|
|
306
|
+
*/
|
|
307
|
+
recordDuration(durationMs) {
|
|
308
|
+
this.recentDurations.push(durationMs);
|
|
309
|
+
if (this.recentDurations.length < this.sampleWindowSize) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const total = this.recentDurations.reduce((sum, val) => sum + val, 0);
|
|
313
|
+
const average = total / this.recentDurations.length;
|
|
314
|
+
this.lastAverageLatencyMs = average;
|
|
315
|
+
this.recentDurations = [];
|
|
316
|
+
if (average > this.targetLatencyMs) {
|
|
317
|
+
const decreased = Math.max(
|
|
318
|
+
this.minConcurrency,
|
|
319
|
+
Math.floor(this._currentConcurrency * this.backoffFactor)
|
|
320
|
+
);
|
|
321
|
+
if (decreased !== this._currentConcurrency) {
|
|
322
|
+
const prev = this._currentConcurrency;
|
|
323
|
+
this._currentConcurrency = decreased;
|
|
324
|
+
this.onConcurrencyChange(
|
|
325
|
+
prev,
|
|
326
|
+
decreased,
|
|
327
|
+
`Average latency (${Math.round(average)}ms) exceeded target (${this.targetLatencyMs}ms). Scaled down.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} else if (average < this.targetLatencyMs * 0.75) {
|
|
331
|
+
const increased = Math.min(this.maxConcurrency, this._currentConcurrency + 1);
|
|
332
|
+
if (increased !== this._currentConcurrency) {
|
|
333
|
+
const prev = this._currentConcurrency;
|
|
334
|
+
this._currentConcurrency = increased;
|
|
335
|
+
this.onConcurrencyChange(
|
|
336
|
+
prev,
|
|
337
|
+
increased,
|
|
338
|
+
`Average latency (${Math.round(average)}ms) below target threshold. Scaled up.`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Returns a snapshot of adaptive telemetry metrics.
|
|
345
|
+
*/
|
|
346
|
+
getStats() {
|
|
347
|
+
return {
|
|
348
|
+
currentConcurrency: this._currentConcurrency,
|
|
349
|
+
averageLatencyMs: this.lastAverageLatencyMs,
|
|
350
|
+
samplesRecorded: this.recentDurations.length
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/models/circuit-breaker.model.ts
|
|
356
|
+
var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
|
|
357
|
+
ECircuitState2["CLOSED"] = "closed";
|
|
358
|
+
ECircuitState2["OPEN"] = "open";
|
|
359
|
+
ECircuitState2["HALF_OPEN"] = "half_open";
|
|
360
|
+
return ECircuitState2;
|
|
361
|
+
})(ECircuitState || {});
|
|
362
|
+
|
|
363
|
+
// src/scheduler/circuit-breaker.ts
|
|
364
|
+
var CircuitBreakerCoordinator = class {
|
|
365
|
+
_state = "closed" /* CLOSED */;
|
|
366
|
+
_consecutiveFailures = 0;
|
|
367
|
+
_lastFailureTime;
|
|
368
|
+
failureThreshold;
|
|
369
|
+
resetTimeoutMs;
|
|
370
|
+
/**
|
|
371
|
+
* Initializes a new CircuitBreakerCoordinator.
|
|
372
|
+
*
|
|
373
|
+
* @param options - Configuration options for threshold and cool-down window.
|
|
374
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
375
|
+
*/
|
|
376
|
+
constructor(options) {
|
|
377
|
+
if (typeof options.failureThreshold !== "number" || Number.isNaN(options.failureThreshold) || !Number.isInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
378
|
+
throw new AhkoConfigurationError(
|
|
379
|
+
`Invalid failureThreshold "${options.failureThreshold}". failureThreshold must be an integer greater than or equal to 1.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (typeof options.resetTimeoutMs !== "number" || Number.isNaN(options.resetTimeoutMs) || !Number.isFinite(options.resetTimeoutMs) || options.resetTimeoutMs <= 0) {
|
|
383
|
+
throw new AhkoConfigurationError(
|
|
384
|
+
`Invalid resetTimeoutMs "${options.resetTimeoutMs}". resetTimeoutMs must be a positive finite number greater than 0.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
this.failureThreshold = options.failureThreshold;
|
|
388
|
+
this.resetTimeoutMs = options.resetTimeoutMs;
|
|
389
|
+
}
|
|
390
|
+
/** Current state of the circuit breaker */
|
|
391
|
+
get state() {
|
|
392
|
+
this.refreshState();
|
|
393
|
+
return this._state;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Checks whether an execution is currently allowed.
|
|
397
|
+
* If the circuit is OPEN and cool-down has not elapsed, fast-fails immediately.
|
|
398
|
+
*
|
|
399
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit is currently OPEN.
|
|
400
|
+
*/
|
|
401
|
+
checkAllowed() {
|
|
402
|
+
this.refreshState();
|
|
403
|
+
if (this._state === "open" /* OPEN */) {
|
|
404
|
+
const remainingMs = this._lastFailureTime ? Math.max(0, this.resetTimeoutMs - (Date.now() - this._lastFailureTime)) : this.resetTimeoutMs;
|
|
405
|
+
throw new AhkoCircuitBreakerOpenError(
|
|
406
|
+
`Circuit breaker is open. Fast-failing task execution. Remaining cool-down: ${remainingMs}ms.`,
|
|
407
|
+
{
|
|
408
|
+
resetTimeoutMs: remainingMs,
|
|
409
|
+
trippedAt: this._lastFailureTime,
|
|
410
|
+
consecutiveFailures: this._consecutiveFailures
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Records a successful task execution.
|
|
417
|
+
* Heals HALF_OPEN state back to CLOSED and resets consecutive failure counters.
|
|
418
|
+
*/
|
|
419
|
+
recordSuccess() {
|
|
420
|
+
this._consecutiveFailures = 0;
|
|
421
|
+
this._state = "closed" /* CLOSED */;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Records a failed task execution.
|
|
425
|
+
* Trips CLOSED to OPEN when threshold is met, or re-trips HALF_OPEN immediately.
|
|
426
|
+
*
|
|
427
|
+
* @param _error - Optional error that caused the failure.
|
|
428
|
+
*/
|
|
429
|
+
recordFailure(_error) {
|
|
430
|
+
this._consecutiveFailures++;
|
|
431
|
+
this._lastFailureTime = Date.now();
|
|
432
|
+
if (this._state === "half_open" /* HALF_OPEN */) {
|
|
433
|
+
this._state = "open" /* OPEN */;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (this._consecutiveFailures >= this.failureThreshold) {
|
|
437
|
+
this._state = "open" /* OPEN */;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Evaluates if enough time has passed to transition from OPEN to HALF_OPEN.
|
|
442
|
+
*/
|
|
443
|
+
refreshState() {
|
|
444
|
+
if (this._state === "open" /* OPEN */ && this._lastFailureTime !== void 0) {
|
|
445
|
+
const elapsed = Date.now() - this._lastFailureTime;
|
|
446
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
447
|
+
this._state = "half_open" /* HALF_OPEN */;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Resets the circuit breaker back to initial CLOSED state.
|
|
453
|
+
*/
|
|
454
|
+
reset() {
|
|
455
|
+
this._state = "closed" /* CLOSED */;
|
|
456
|
+
this._consecutiveFailures = 0;
|
|
457
|
+
this._lastFailureTime = void 0;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Returns a snapshot of circuit breaker telemetry.
|
|
461
|
+
*/
|
|
462
|
+
getStats() {
|
|
463
|
+
this.refreshState();
|
|
464
|
+
return {
|
|
465
|
+
state: this._state,
|
|
466
|
+
consecutiveFailures: this._consecutiveFailures,
|
|
467
|
+
lastFailureTime: this._lastFailureTime
|
|
468
|
+
};
|
|
109
469
|
}
|
|
110
470
|
};
|
|
111
471
|
|
|
@@ -505,7 +865,7 @@ var AhkoEventEmitter = class {
|
|
|
505
865
|
// src/scheduler/task-queue.ts
|
|
506
866
|
var TaskQueue = class {
|
|
507
867
|
/** Maximum concurrent active tasks */
|
|
508
|
-
|
|
868
|
+
_concurrency;
|
|
509
869
|
/** Minimum interval in milliseconds between consecutive task starts */
|
|
510
870
|
minIntervalMs;
|
|
511
871
|
/** Timestamp of the most recent task start */
|
|
@@ -522,12 +882,20 @@ var TaskQueue = class {
|
|
|
522
882
|
idleEntries = /* @__PURE__ */ new Set();
|
|
523
883
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
524
884
|
retryEntries = /* @__PURE__ */ new Set();
|
|
885
|
+
/** Tag index for selective cancellation and task classification */
|
|
886
|
+
tagIndex = /* @__PURE__ */ new Map();
|
|
525
887
|
/** Coordinator for debounced tasks with key coalescing */
|
|
526
888
|
debounceCoordinator = new DebounceCoordinator();
|
|
527
889
|
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
528
890
|
throttleCoordinator = new ThrottleCoordinator();
|
|
529
891
|
/** Lifecycle event emitter for task and scheduler events */
|
|
530
892
|
emitter = new AhkoEventEmitter();
|
|
893
|
+
/** Circuit breaker coordinator if configured */
|
|
894
|
+
circuitBreakerCoordinator;
|
|
895
|
+
/** Adaptive concurrency coordinator if configured */
|
|
896
|
+
adaptiveCoordinator;
|
|
897
|
+
/** Pause state flag */
|
|
898
|
+
_isPaused = false;
|
|
531
899
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
532
900
|
idleResolvers = /* @__PURE__ */ new Set();
|
|
533
901
|
/** WeakMap associating task runners with their scheduling options */
|
|
@@ -549,9 +917,11 @@ var TaskQueue = class {
|
|
|
549
917
|
*
|
|
550
918
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
551
919
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
920
|
+
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
921
|
+
* @param adaptiveOptions - Optional adaptive concurrency policy configuration.
|
|
552
922
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
553
923
|
*/
|
|
554
|
-
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
924
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions, adaptiveOptions) {
|
|
555
925
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
556
926
|
throw new AhkoConfigurationError(
|
|
557
927
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -562,11 +932,171 @@ var TaskQueue = class {
|
|
|
562
932
|
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
563
933
|
);
|
|
564
934
|
}
|
|
565
|
-
this.
|
|
935
|
+
this._concurrency = concurrency;
|
|
566
936
|
this.minIntervalMs = minIntervalMs;
|
|
937
|
+
if (circuitBreakerOptions) {
|
|
938
|
+
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
939
|
+
}
|
|
940
|
+
if (adaptiveOptions) {
|
|
941
|
+
this.adaptiveCoordinator = new AdaptiveCoordinator(
|
|
942
|
+
adaptiveOptions,
|
|
943
|
+
this._concurrency,
|
|
944
|
+
(previous, current, reason) => {
|
|
945
|
+
this._concurrency = current;
|
|
946
|
+
this.emitter.emit("concurrency:change", {
|
|
947
|
+
previousConcurrency: previous,
|
|
948
|
+
currentConcurrency: current,
|
|
949
|
+
reason
|
|
950
|
+
});
|
|
951
|
+
this.pump();
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
this._concurrency = this.adaptiveCoordinator.currentConcurrency;
|
|
955
|
+
}
|
|
567
956
|
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
568
957
|
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
569
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Current concurrency capacity limit.
|
|
961
|
+
*/
|
|
962
|
+
get concurrency() {
|
|
963
|
+
return this._concurrency;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Dynamically adjusts the concurrency limit at runtime.
|
|
967
|
+
*
|
|
968
|
+
* @param newConcurrency - New maximum concurrency (must be >= 1).
|
|
969
|
+
* @throws {AhkoConfigurationError} If newConcurrency is less than 1.
|
|
970
|
+
*/
|
|
971
|
+
setConcurrency(newConcurrency) {
|
|
972
|
+
if (Number.isNaN(newConcurrency) || newConcurrency < 1) {
|
|
973
|
+
throw new AhkoConfigurationError(
|
|
974
|
+
`Invalid concurrency "${newConcurrency}". Must be a number greater than or equal to 1.`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const previous = this._concurrency;
|
|
978
|
+
this._concurrency = newConcurrency;
|
|
979
|
+
if (this.adaptiveCoordinator) {
|
|
980
|
+
this.adaptiveCoordinator.setConcurrency(newConcurrency);
|
|
981
|
+
}
|
|
982
|
+
if (newConcurrency !== previous) {
|
|
983
|
+
this.emitter.emit("concurrency:change", {
|
|
984
|
+
previousConcurrency: previous,
|
|
985
|
+
currentConcurrency: newConcurrency,
|
|
986
|
+
reason: "Manual concurrency update"
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
this.pump();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
|
|
993
|
+
*/
|
|
994
|
+
pause() {
|
|
995
|
+
this._isPaused = true;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Resumes queue execution, immediately dispatching waiting tasks up to available concurrency.
|
|
999
|
+
*/
|
|
1000
|
+
resume() {
|
|
1001
|
+
if (this._isPaused) {
|
|
1002
|
+
this._isPaused = false;
|
|
1003
|
+
this.pump();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Checks whether the task queue is currently paused.
|
|
1008
|
+
*/
|
|
1009
|
+
isPaused() {
|
|
1010
|
+
return this._isPaused;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Cancels all pending, delayed, and active tasks marked with the specified tag.
|
|
1014
|
+
*
|
|
1015
|
+
* @param tag - Tag identifier to match.
|
|
1016
|
+
* @param reason - Optional cancellation reason.
|
|
1017
|
+
* @returns Total count of tasks cancelled.
|
|
1018
|
+
*/
|
|
1019
|
+
cancelByTag(tag, reason) {
|
|
1020
|
+
const runners = this.tagIndex.get(tag);
|
|
1021
|
+
if (!runners || runners.size === 0) {
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
const list = Array.from(runners);
|
|
1025
|
+
let count = 0;
|
|
1026
|
+
for (const runner of list) {
|
|
1027
|
+
if (runner.state === "pending" /* PENDING */ || runner.state === "running" /* RUNNING */) {
|
|
1028
|
+
runner.cancel(reason ?? `Task cancelled by tag "${tag}"`);
|
|
1029
|
+
count++;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return count;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Returns active and pending task counts for a given tag.
|
|
1036
|
+
*
|
|
1037
|
+
* @param tag - Tag identifier.
|
|
1038
|
+
*/
|
|
1039
|
+
getStatsByTag(tag) {
|
|
1040
|
+
const runners = this.tagIndex.get(tag);
|
|
1041
|
+
if (!runners) {
|
|
1042
|
+
return { activeTasks: 0, pendingTasks: 0 };
|
|
1043
|
+
}
|
|
1044
|
+
let active = 0;
|
|
1045
|
+
let pending = 0;
|
|
1046
|
+
for (const runner of runners) {
|
|
1047
|
+
if (runner.state === "running" /* RUNNING */) {
|
|
1048
|
+
active++;
|
|
1049
|
+
} else if (runner.state === "pending" /* PENDING */) {
|
|
1050
|
+
pending++;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return { activeTasks: active, pendingTasks: pending };
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Indexes a runner under all its associated tags.
|
|
1057
|
+
*/
|
|
1058
|
+
indexTaskTags(runner) {
|
|
1059
|
+
for (const tag of runner.tags) {
|
|
1060
|
+
let set = this.tagIndex.get(tag);
|
|
1061
|
+
if (!set) {
|
|
1062
|
+
set = /* @__PURE__ */ new Set();
|
|
1063
|
+
this.tagIndex.set(tag, set);
|
|
1064
|
+
}
|
|
1065
|
+
set.add(runner);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Removes a runner from the tag index upon settlement.
|
|
1070
|
+
*/
|
|
1071
|
+
cleanupTaskTags(runner) {
|
|
1072
|
+
for (const tag of runner.tags) {
|
|
1073
|
+
const set = this.tagIndex.get(tag);
|
|
1074
|
+
if (set) {
|
|
1075
|
+
set.delete(runner);
|
|
1076
|
+
if (set.size === 0) {
|
|
1077
|
+
this.tagIndex.delete(tag);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Inserts a task runner into the queue based on priority weight (descending).
|
|
1084
|
+
* Preserves FIFO ordering among tasks with identical priority.
|
|
1085
|
+
*/
|
|
1086
|
+
insertIntoQueue(runner) {
|
|
1087
|
+
const options = this.runnerOptions.get(runner);
|
|
1088
|
+
const targetWeight = resolvePriorityWeight(options?.priority);
|
|
1089
|
+
let insertIndex = this.queue.length;
|
|
1090
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
1091
|
+
const existingOptions = this.runnerOptions.get(this.queue[i]);
|
|
1092
|
+
const existingWeight = resolvePriorityWeight(existingOptions?.priority);
|
|
1093
|
+
if (existingWeight < targetWeight) {
|
|
1094
|
+
insertIndex = i;
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
this.queue.splice(insertIndex, 0, runner);
|
|
1099
|
+
}
|
|
570
1100
|
/**
|
|
571
1101
|
* Enqueues a task runner according to the specified schedule options.
|
|
572
1102
|
*
|
|
@@ -620,13 +1150,35 @@ var TaskQueue = class {
|
|
|
620
1150
|
);
|
|
621
1151
|
}
|
|
622
1152
|
}
|
|
1153
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
1154
|
+
if (typeof options.totalTimeoutMs !== "number" || Number.isNaN(options.totalTimeoutMs) || !Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0) {
|
|
1155
|
+
throw new AhkoConfigurationError(
|
|
1156
|
+
`Invalid totalTimeoutMs "${options.totalTimeoutMs}". totalTimeoutMs must be a positive finite number greater than 0.`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
623
1160
|
if (options) {
|
|
624
1161
|
this.runnerOptions.set(runner, options);
|
|
625
1162
|
}
|
|
1163
|
+
this.indexTaskTags(runner);
|
|
1164
|
+
runner.promise.finally(() => {
|
|
1165
|
+
this.cleanupTaskTags(runner);
|
|
1166
|
+
}).catch(() => {
|
|
1167
|
+
});
|
|
626
1168
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
627
1169
|
this.cancelledTasks++;
|
|
628
1170
|
return runner.promise;
|
|
629
1171
|
}
|
|
1172
|
+
if (options?.totalTimeoutMs !== void 0) {
|
|
1173
|
+
const budgetMs = options.totalTimeoutMs;
|
|
1174
|
+
const totalTimerId = setTimeout(() => {
|
|
1175
|
+
runner.timeout(budgetMs, `Task total execution deadline exceeded after ${budgetMs}ms`);
|
|
1176
|
+
}, budgetMs);
|
|
1177
|
+
runner.promise.finally(() => {
|
|
1178
|
+
clearTimeout(totalTimerId);
|
|
1179
|
+
}).catch(() => {
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
630
1182
|
if (strategy === "delay" /* DELAY */) {
|
|
631
1183
|
const delayMs = options?.delay ?? 0;
|
|
632
1184
|
if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
|
|
@@ -650,15 +1202,23 @@ var TaskQueue = class {
|
|
|
650
1202
|
const index = this.queue.indexOf(runner);
|
|
651
1203
|
if (index !== -1) {
|
|
652
1204
|
this.queue.splice(index, 1);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1205
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1206
|
+
this.timedOutTasks++;
|
|
1207
|
+
this.emitter.emit("task:timeout", {
|
|
1208
|
+
taskId: runner.taskId,
|
|
1209
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1210
|
+
});
|
|
1211
|
+
} else {
|
|
1212
|
+
this.cancelledTasks++;
|
|
1213
|
+
this.emitter.emit("task:cancel", {
|
|
1214
|
+
taskId: runner.taskId,
|
|
1215
|
+
reason: "Task cancelled while queued"
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
658
1218
|
this.checkIdle();
|
|
659
1219
|
}
|
|
660
1220
|
};
|
|
661
|
-
this.
|
|
1221
|
+
this.insertIntoQueue(runner);
|
|
662
1222
|
this.pump();
|
|
663
1223
|
return runner.promise;
|
|
664
1224
|
}
|
|
@@ -671,22 +1231,30 @@ var TaskQueue = class {
|
|
|
671
1231
|
runner,
|
|
672
1232
|
timerId: setTimeout(() => {
|
|
673
1233
|
this.delayedEntries.delete(delayedEntry);
|
|
674
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1234
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
675
1235
|
return;
|
|
676
1236
|
}
|
|
677
1237
|
runner.onCancel = () => {
|
|
678
1238
|
const index = this.queue.indexOf(runner);
|
|
679
1239
|
if (index !== -1) {
|
|
680
1240
|
this.queue.splice(index, 1);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1241
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1242
|
+
this.timedOutTasks++;
|
|
1243
|
+
this.emitter.emit("task:timeout", {
|
|
1244
|
+
taskId: runner.taskId,
|
|
1245
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
this.cancelledTasks++;
|
|
1249
|
+
this.emitter.emit("task:cancel", {
|
|
1250
|
+
taskId: runner.taskId,
|
|
1251
|
+
reason: "Task cancelled while queued"
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
686
1254
|
this.checkIdle();
|
|
687
1255
|
}
|
|
688
1256
|
};
|
|
689
|
-
this.
|
|
1257
|
+
this.insertIntoQueue(runner);
|
|
690
1258
|
this.pump();
|
|
691
1259
|
}, delayMs)
|
|
692
1260
|
};
|
|
@@ -695,11 +1263,19 @@ var TaskQueue = class {
|
|
|
695
1263
|
if (this.delayedEntries.has(delayedEntry)) {
|
|
696
1264
|
clearTimeout(delayedEntry.timerId);
|
|
697
1265
|
this.delayedEntries.delete(delayedEntry);
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1266
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1267
|
+
this.timedOutTasks++;
|
|
1268
|
+
this.emitter.emit("task:timeout", {
|
|
1269
|
+
taskId: runner.taskId,
|
|
1270
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1271
|
+
});
|
|
1272
|
+
} else {
|
|
1273
|
+
this.cancelledTasks++;
|
|
1274
|
+
this.emitter.emit("task:cancel", {
|
|
1275
|
+
taskId: runner.taskId,
|
|
1276
|
+
reason: "Task cancelled while waiting in delay"
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
703
1279
|
this.checkIdle();
|
|
704
1280
|
}
|
|
705
1281
|
};
|
|
@@ -712,22 +1288,30 @@ var TaskQueue = class {
|
|
|
712
1288
|
let idleEntry;
|
|
713
1289
|
const handle = IdleScheduler.schedule(() => {
|
|
714
1290
|
this.idleEntries.delete(idleEntry);
|
|
715
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1291
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
716
1292
|
return;
|
|
717
1293
|
}
|
|
718
1294
|
runner.onCancel = () => {
|
|
719
1295
|
const index = this.queue.indexOf(runner);
|
|
720
1296
|
if (index !== -1) {
|
|
721
1297
|
this.queue.splice(index, 1);
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1298
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1299
|
+
this.timedOutTasks++;
|
|
1300
|
+
this.emitter.emit("task:timeout", {
|
|
1301
|
+
taskId: runner.taskId,
|
|
1302
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1303
|
+
});
|
|
1304
|
+
} else {
|
|
1305
|
+
this.cancelledTasks++;
|
|
1306
|
+
this.emitter.emit("task:cancel", {
|
|
1307
|
+
taskId: runner.taskId,
|
|
1308
|
+
reason: "Task cancelled while queued"
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
727
1311
|
this.checkIdle();
|
|
728
1312
|
}
|
|
729
1313
|
};
|
|
730
|
-
this.
|
|
1314
|
+
this.insertIntoQueue(runner);
|
|
731
1315
|
this.pump();
|
|
732
1316
|
}, idleTimeout);
|
|
733
1317
|
idleEntry = { runner, handle };
|
|
@@ -736,21 +1320,30 @@ var TaskQueue = class {
|
|
|
736
1320
|
if (this.idleEntries.has(idleEntry)) {
|
|
737
1321
|
handle.cancel();
|
|
738
1322
|
this.idleEntries.delete(idleEntry);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1323
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1324
|
+
this.timedOutTasks++;
|
|
1325
|
+
this.emitter.emit("task:timeout", {
|
|
1326
|
+
taskId: runner.taskId,
|
|
1327
|
+
timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
|
|
1328
|
+
});
|
|
1329
|
+
} else {
|
|
1330
|
+
this.cancelledTasks++;
|
|
1331
|
+
this.emitter.emit("task:cancel", {
|
|
1332
|
+
taskId: runner.taskId,
|
|
1333
|
+
reason: "Task cancelled while waiting for idle"
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
744
1336
|
this.checkIdle();
|
|
745
1337
|
}
|
|
746
1338
|
};
|
|
747
1339
|
}
|
|
748
1340
|
/**
|
|
749
1341
|
* Pumps the queue by picking pending tasks and executing them
|
|
750
|
-
* as long as concurrency capacity is available
|
|
1342
|
+
* as long as concurrency capacity is available, minIntervalMs is respected,
|
|
1343
|
+
* and queue is not paused.
|
|
751
1344
|
*/
|
|
752
1345
|
pump() {
|
|
753
|
-
if (this.queue.length === 0 || this.activeRunners.size >= this.
|
|
1346
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this._concurrency) {
|
|
754
1347
|
return;
|
|
755
1348
|
}
|
|
756
1349
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -767,7 +1360,7 @@ var TaskQueue = class {
|
|
|
767
1360
|
return;
|
|
768
1361
|
}
|
|
769
1362
|
}
|
|
770
|
-
while (this.activeRunners.size < this.
|
|
1363
|
+
while (!this._isPaused && this.activeRunners.size < this._concurrency && this.queue.length > 0) {
|
|
771
1364
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
772
1365
|
const now = Date.now();
|
|
773
1366
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -786,14 +1379,30 @@ var TaskQueue = class {
|
|
|
786
1379
|
if (!runner) {
|
|
787
1380
|
break;
|
|
788
1381
|
}
|
|
789
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1382
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
790
1383
|
continue;
|
|
791
1384
|
}
|
|
1385
|
+
if (this.circuitBreakerCoordinator) {
|
|
1386
|
+
try {
|
|
1387
|
+
this.circuitBreakerCoordinator.checkAllowed();
|
|
1388
|
+
} catch (cbError) {
|
|
1389
|
+
this.failedTasks++;
|
|
1390
|
+
this.runnerOptions.delete(runner);
|
|
1391
|
+
this.emitter.emit("task:fail", {
|
|
1392
|
+
taskId: runner.taskId,
|
|
1393
|
+
attempt: runner.attempt,
|
|
1394
|
+
error: cbError,
|
|
1395
|
+
willRetry: false
|
|
1396
|
+
});
|
|
1397
|
+
runner.reject(cbError);
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
792
1401
|
this.activeRunners.add(runner);
|
|
793
1402
|
this.lastTaskStartTime = Date.now();
|
|
794
1403
|
void this.executeRunner(runner);
|
|
795
1404
|
if (this.minIntervalMs > 0) {
|
|
796
|
-
if (this.queue.length > 0 && this.activeRunners.size < this.
|
|
1405
|
+
if (this.queue.length > 0 && this.activeRunners.size < this._concurrency) {
|
|
797
1406
|
if (this.rateLimitTimer === void 0) {
|
|
798
1407
|
this.rateLimitTimer = setTimeout(() => {
|
|
799
1408
|
this.rateLimitTimer = void 0;
|
|
@@ -817,6 +1426,8 @@ var TaskQueue = class {
|
|
|
817
1426
|
});
|
|
818
1427
|
try {
|
|
819
1428
|
const result = await runner.run();
|
|
1429
|
+
this.circuitBreakerCoordinator?.recordSuccess();
|
|
1430
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
820
1431
|
this.completedTasks++;
|
|
821
1432
|
this.activeRunners.delete(runner);
|
|
822
1433
|
this.runnerOptions.delete(runner);
|
|
@@ -852,11 +1463,15 @@ var TaskQueue = class {
|
|
|
852
1463
|
this.scheduleRetry(runner, options);
|
|
853
1464
|
return;
|
|
854
1465
|
}
|
|
1466
|
+
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1467
|
+
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1468
|
+
}
|
|
1469
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
855
1470
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
856
1471
|
this.timedOutTasks++;
|
|
857
1472
|
this.emitter.emit("task:timeout", {
|
|
858
1473
|
taskId: runner.taskId,
|
|
859
|
-
timeoutMs: runner.timeoutMs
|
|
1474
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
860
1475
|
});
|
|
861
1476
|
} else {
|
|
862
1477
|
this.failedTasks++;
|
|
@@ -886,15 +1501,23 @@ var TaskQueue = class {
|
|
|
886
1501
|
const index = this.queue.indexOf(runner);
|
|
887
1502
|
if (index !== -1) {
|
|
888
1503
|
this.queue.splice(index, 1);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1504
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1505
|
+
this.timedOutTasks++;
|
|
1506
|
+
this.emitter.emit("task:timeout", {
|
|
1507
|
+
taskId: runner.taskId,
|
|
1508
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
this.cancelledTasks++;
|
|
1512
|
+
this.emitter.emit("task:cancel", {
|
|
1513
|
+
taskId: runner.taskId,
|
|
1514
|
+
reason: "Task cancelled while queued"
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
894
1517
|
this.checkIdle();
|
|
895
1518
|
}
|
|
896
1519
|
};
|
|
897
|
-
this.
|
|
1520
|
+
this.insertIntoQueue(runner);
|
|
898
1521
|
this.pump();
|
|
899
1522
|
return;
|
|
900
1523
|
}
|
|
@@ -902,22 +1525,30 @@ var TaskQueue = class {
|
|
|
902
1525
|
runner,
|
|
903
1526
|
timerId: setTimeout(() => {
|
|
904
1527
|
this.retryEntries.delete(retryEntry);
|
|
905
|
-
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
1528
|
+
if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
906
1529
|
return;
|
|
907
1530
|
}
|
|
908
1531
|
runner.onCancel = () => {
|
|
909
1532
|
const index = this.queue.indexOf(runner);
|
|
910
1533
|
if (index !== -1) {
|
|
911
1534
|
this.queue.splice(index, 1);
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1535
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1536
|
+
this.timedOutTasks++;
|
|
1537
|
+
this.emitter.emit("task:timeout", {
|
|
1538
|
+
taskId: runner.taskId,
|
|
1539
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1540
|
+
});
|
|
1541
|
+
} else {
|
|
1542
|
+
this.cancelledTasks++;
|
|
1543
|
+
this.emitter.emit("task:cancel", {
|
|
1544
|
+
taskId: runner.taskId,
|
|
1545
|
+
reason: "Task cancelled while queued"
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
917
1548
|
this.checkIdle();
|
|
918
1549
|
}
|
|
919
1550
|
};
|
|
920
|
-
this.
|
|
1551
|
+
this.insertIntoQueue(runner);
|
|
921
1552
|
this.pump();
|
|
922
1553
|
}, backoffDelay)
|
|
923
1554
|
};
|
|
@@ -926,11 +1557,19 @@ var TaskQueue = class {
|
|
|
926
1557
|
if (this.retryEntries.has(retryEntry)) {
|
|
927
1558
|
clearTimeout(retryEntry.timerId);
|
|
928
1559
|
this.retryEntries.delete(retryEntry);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1560
|
+
if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
|
|
1561
|
+
this.timedOutTasks++;
|
|
1562
|
+
this.emitter.emit("task:timeout", {
|
|
1563
|
+
taskId: runner.taskId,
|
|
1564
|
+
timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
|
|
1565
|
+
});
|
|
1566
|
+
} else {
|
|
1567
|
+
this.cancelledTasks++;
|
|
1568
|
+
this.emitter.emit("task:cancel", {
|
|
1569
|
+
taskId: runner.taskId,
|
|
1570
|
+
reason: "Task cancelled during retry backoff"
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
934
1573
|
this.checkIdle();
|
|
935
1574
|
}
|
|
936
1575
|
};
|
|
@@ -977,7 +1616,7 @@ var TaskQueue = class {
|
|
|
977
1616
|
clear() {
|
|
978
1617
|
while (this.queue.length > 0) {
|
|
979
1618
|
const runner = this.queue.shift();
|
|
980
|
-
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1619
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
|
|
981
1620
|
runner.cancel("Scheduler cleared");
|
|
982
1621
|
this.cancelledTasks++;
|
|
983
1622
|
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
@@ -1027,7 +1666,10 @@ var TaskQueue = class {
|
|
|
1027
1666
|
timedOutTasks: this.timedOutTasks,
|
|
1028
1667
|
retriedTasks: this.retriedTasks,
|
|
1029
1668
|
totalDispatched: this.totalDispatched,
|
|
1030
|
-
capacity: this.
|
|
1669
|
+
capacity: this._concurrency,
|
|
1670
|
+
isPaused: this._isPaused,
|
|
1671
|
+
circuitState: this.circuitBreakerCoordinator?.state,
|
|
1672
|
+
adaptive: this.adaptiveCoordinator?.getStats()
|
|
1031
1673
|
});
|
|
1032
1674
|
}
|
|
1033
1675
|
};
|
|
@@ -1063,18 +1705,22 @@ var TaskRunner = class {
|
|
|
1063
1705
|
attempt = 1;
|
|
1064
1706
|
/** Duration of the most recent execution attempt in milliseconds */
|
|
1065
1707
|
lastDurationMs = 0;
|
|
1708
|
+
/** Set of classification tags associated with this task */
|
|
1709
|
+
tags;
|
|
1066
1710
|
/**
|
|
1067
1711
|
* Creates a new TaskRunner instance.
|
|
1068
1712
|
*
|
|
1069
1713
|
* @param task - The asynchronous work unit to run.
|
|
1070
1714
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
1071
1715
|
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
1716
|
+
* @param tags - Optional array of tags for classifying and selectively cancelling tasks.
|
|
1072
1717
|
*/
|
|
1073
|
-
constructor(task, externalSignal, timeoutMs) {
|
|
1718
|
+
constructor(task, externalSignal, timeoutMs, tags) {
|
|
1074
1719
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1075
1720
|
this.task = task;
|
|
1076
1721
|
this.externalSignal = externalSignal;
|
|
1077
1722
|
this.timeoutMs = timeoutMs;
|
|
1723
|
+
this.tags = new Set(tags ?? []);
|
|
1078
1724
|
this.abortController = new AbortController();
|
|
1079
1725
|
this.promise = new Promise((resolve, reject) => {
|
|
1080
1726
|
this.resolvePromise = resolve;
|
|
@@ -1098,6 +1744,8 @@ var TaskRunner = class {
|
|
|
1098
1744
|
}
|
|
1099
1745
|
}
|
|
1100
1746
|
}
|
|
1747
|
+
/** Flag indicating if runner was aborted by an overall total timeout deadline */
|
|
1748
|
+
totalTimedOut = false;
|
|
1101
1749
|
/**
|
|
1102
1750
|
* Gets the current lifecycle state of the task.
|
|
1103
1751
|
*/
|
|
@@ -1130,7 +1778,7 @@ var TaskRunner = class {
|
|
|
1130
1778
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
1131
1779
|
*/
|
|
1132
1780
|
async canRetry(error, retryOptions) {
|
|
1133
|
-
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1781
|
+
if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
1134
1782
|
return false;
|
|
1135
1783
|
}
|
|
1136
1784
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -1171,15 +1819,16 @@ var TaskRunner = class {
|
|
|
1171
1819
|
let abortListener;
|
|
1172
1820
|
const abortPromise = new Promise((_, reject) => {
|
|
1173
1821
|
abortListener = () => {
|
|
1174
|
-
|
|
1822
|
+
const reason = this.abortController.signal.reason;
|
|
1823
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || reason instanceof AhkoTimeoutError) {
|
|
1824
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1175
1825
|
reject(
|
|
1176
|
-
new AhkoTimeoutError(
|
|
1826
|
+
reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
|
|
1177
1827
|
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1178
1828
|
{ timeoutMs: this.timeoutMs }
|
|
1179
1829
|
)
|
|
1180
1830
|
);
|
|
1181
1831
|
} else {
|
|
1182
|
-
const reason = this.abortController.signal.reason;
|
|
1183
1832
|
reject(
|
|
1184
1833
|
new AhkoCancellationError("Task was cancelled during execution", {
|
|
1185
1834
|
cause: reason instanceof Error ? reason : void 0
|
|
@@ -1214,6 +1863,10 @@ var TaskRunner = class {
|
|
|
1214
1863
|
}
|
|
1215
1864
|
taskExecutionPromise.catch(() => {
|
|
1216
1865
|
});
|
|
1866
|
+
abortPromise.catch(() => {
|
|
1867
|
+
});
|
|
1868
|
+
timeoutPromise?.catch(() => {
|
|
1869
|
+
});
|
|
1217
1870
|
const racePromises = [
|
|
1218
1871
|
taskExecutionPromise,
|
|
1219
1872
|
abortPromise
|
|
@@ -1305,6 +1958,31 @@ var TaskRunner = class {
|
|
|
1305
1958
|
this.onCancel?.(this);
|
|
1306
1959
|
}
|
|
1307
1960
|
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Times out the task, aborting pending or running execution with AhkoTimeoutError.
|
|
1963
|
+
*
|
|
1964
|
+
* @param timeoutMs - Timeout duration in milliseconds.
|
|
1965
|
+
* @param message - Optional custom timeout message.
|
|
1966
|
+
*/
|
|
1967
|
+
timeout(timeoutMs, message) {
|
|
1968
|
+
if (this._state === "completed" /* COMPLETED */ || this._state === "failed" /* FAILED */ || this._state === "cancelled" /* CANCELLED */ || this._state === "timed_out" /* TIMED_OUT */) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const wasPending = this._state === "pending" /* PENDING */;
|
|
1972
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1973
|
+
this.totalTimedOut = true;
|
|
1974
|
+
this.clearTimeoutTimer();
|
|
1975
|
+
const timeoutError = new AhkoTimeoutError(
|
|
1976
|
+
message ?? `Task execution timed out after ${timeoutMs}ms`,
|
|
1977
|
+
{ timeoutMs }
|
|
1978
|
+
);
|
|
1979
|
+
this.abortController.abort(timeoutError);
|
|
1980
|
+
this.cleanup();
|
|
1981
|
+
if (wasPending) {
|
|
1982
|
+
this.rejectPromise(timeoutError);
|
|
1983
|
+
this.onCancel?.(this);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1308
1986
|
/**
|
|
1309
1987
|
* Handles external AbortSignal trigger.
|
|
1310
1988
|
*/
|
|
@@ -1323,9 +2001,56 @@ var TaskRunner = class {
|
|
|
1323
2001
|
};
|
|
1324
2002
|
|
|
1325
2003
|
// src/ahko.ts
|
|
1326
|
-
var Ahko = class {
|
|
2004
|
+
var Ahko = class _Ahko {
|
|
1327
2005
|
/** Internal queue and concurrency manager */
|
|
1328
2006
|
queue;
|
|
2007
|
+
/** Default schedule options inherited from profile if configured */
|
|
2008
|
+
defaultScheduleOptions;
|
|
2009
|
+
/**
|
|
2010
|
+
* Programmatically loads a declarative configuration into memory.
|
|
2011
|
+
* Works universally across Node.js, browsers, and edge runtimes.
|
|
2012
|
+
*
|
|
2013
|
+
* @param config - File configuration object containing default and named profiles.
|
|
2014
|
+
*/
|
|
2015
|
+
static loadConfig(config) {
|
|
2016
|
+
loadConfig(config);
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Asynchronously loads a configuration file from disk (Node.js).
|
|
2020
|
+
*
|
|
2021
|
+
* @param filePath - Path to configuration file (default: "config.ahko.json").
|
|
2022
|
+
*/
|
|
2023
|
+
static async loadConfigFile(filePath) {
|
|
2024
|
+
return loadConfigFile(filePath);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Resets the active declarative configuration.
|
|
2028
|
+
*/
|
|
2029
|
+
static resetConfig() {
|
|
2030
|
+
resetConfig();
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Retrieves the currently active declarative configuration.
|
|
2034
|
+
*/
|
|
2035
|
+
static getActiveConfig() {
|
|
2036
|
+
return getActiveConfig();
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Instantiates an Ahko scheduler initialized with settings from a declarative profile.
|
|
2040
|
+
*
|
|
2041
|
+
* @param profileName - Optional name of the profile (e.g. "api", "background").
|
|
2042
|
+
* @param overrides - Optional scheduler options overriding profile values.
|
|
2043
|
+
* @returns A new configured Ahko instance.
|
|
2044
|
+
*/
|
|
2045
|
+
static fromProfile(profileName, overrides) {
|
|
2046
|
+
const profile = getProfileConfig(profileName);
|
|
2047
|
+
return new _Ahko({
|
|
2048
|
+
...profile,
|
|
2049
|
+
...overrides,
|
|
2050
|
+
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2051
|
+
adaptive: overrides?.adaptive ?? profile?.adaptive
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
1329
2054
|
/**
|
|
1330
2055
|
* Initializes a new Ahko scheduler instance.
|
|
1331
2056
|
*
|
|
@@ -1338,79 +2063,177 @@ var Ahko = class {
|
|
|
1338
2063
|
* ```
|
|
1339
2064
|
*/
|
|
1340
2065
|
constructor(options) {
|
|
1341
|
-
|
|
2066
|
+
const profile = options?.profile ? getProfileConfig(options.profile) : getProfileConfig();
|
|
2067
|
+
const mergedOptions = {
|
|
2068
|
+
...profile,
|
|
2069
|
+
...options,
|
|
2070
|
+
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2071
|
+
adaptive: options?.adaptive ?? profile?.adaptive
|
|
2072
|
+
};
|
|
2073
|
+
if (profile) {
|
|
2074
|
+
this.defaultScheduleOptions = {
|
|
2075
|
+
priority: profile.priority,
|
|
2076
|
+
retry: profile.retry,
|
|
2077
|
+
timeoutMs: profile.timeoutMs,
|
|
2078
|
+
totalTimeoutMs: profile.totalTimeoutMs,
|
|
2079
|
+
tags: profile.tags
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
this.queue = new TaskQueue(
|
|
2083
|
+
mergedOptions.concurrency,
|
|
2084
|
+
mergedOptions.minIntervalMs,
|
|
2085
|
+
mergedOptions.circuitBreaker,
|
|
2086
|
+
mergedOptions.adaptive
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Current concurrency limit.
|
|
2091
|
+
*/
|
|
2092
|
+
get concurrency() {
|
|
2093
|
+
return this.queue.concurrency;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Dynamically updates the concurrency limit of the scheduler.
|
|
2097
|
+
*
|
|
2098
|
+
* @param concurrency - New maximum concurrency (must be >= 1).
|
|
2099
|
+
* @throws {AhkoConfigurationError} If concurrency is invalid.
|
|
2100
|
+
*/
|
|
2101
|
+
setConcurrency(concurrency) {
|
|
2102
|
+
this.queue.setConcurrency(concurrency);
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
|
|
2106
|
+
*/
|
|
2107
|
+
pause() {
|
|
2108
|
+
this.queue.pause();
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Resumes scheduler dispatch, immediately executing waiting tasks up to available concurrency.
|
|
2112
|
+
*/
|
|
2113
|
+
resume() {
|
|
2114
|
+
this.queue.resume();
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Checks whether the scheduler is currently paused.
|
|
2118
|
+
*/
|
|
2119
|
+
isPaused() {
|
|
2120
|
+
return this.queue.isPaused();
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Current circuit breaker state if circuit breaker protection is configured.
|
|
2124
|
+
*/
|
|
2125
|
+
get circuitState() {
|
|
2126
|
+
return this.queue.circuitBreakerCoordinator?.state;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Access to the underlying circuit breaker coordinator instance if configured.
|
|
2130
|
+
*/
|
|
2131
|
+
get circuitBreaker() {
|
|
2132
|
+
return this.queue.circuitBreakerCoordinator;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Wraps an async function so every execution is automatically routed through this Ahko scheduler.
|
|
2136
|
+
*
|
|
2137
|
+
* @template TArgs - Parameter types of the wrapped function.
|
|
2138
|
+
* @template TReturn - Return type of the wrapped function.
|
|
2139
|
+
* @param fn - The function to wrap.
|
|
2140
|
+
* @param options - Optional scheduling options applied to every wrapped call.
|
|
2141
|
+
* @returns A wrapped function returning a Promise.
|
|
2142
|
+
*
|
|
2143
|
+
* @example
|
|
2144
|
+
* ```typescript
|
|
2145
|
+
* const fetchUser = ahko.wrap(async (id: string) => api.getUser(id), { priority: "high" });
|
|
2146
|
+
* const user = await fetchUser("usr_123");
|
|
2147
|
+
* ```
|
|
2148
|
+
*/
|
|
2149
|
+
wrap(fn, options) {
|
|
2150
|
+
if (typeof fn !== "function") {
|
|
2151
|
+
throw new AhkoConfigurationError("Target to wrap must be a valid function.");
|
|
2152
|
+
}
|
|
2153
|
+
return (...args) => {
|
|
2154
|
+
return this.schedule(() => fn(...args), options);
|
|
2155
|
+
};
|
|
1342
2156
|
}
|
|
1343
2157
|
/**
|
|
1344
2158
|
* Schedules a task for execution with full return type inference.
|
|
1345
2159
|
*
|
|
1346
2160
|
* @template T - Inferred return type of the task.
|
|
1347
2161
|
* @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
|
|
1348
|
-
* @param options - Task-specific scheduling options such as strategy, delay, and cancellation signal.
|
|
2162
|
+
* @param options - Task-specific scheduling options such as strategy, priority, delay, and cancellation signal.
|
|
1349
2163
|
* @returns A promise that resolves with the task's return value.
|
|
1350
2164
|
*
|
|
1351
2165
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
1352
2166
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1353
|
-
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
2167
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs or totalTimeoutMs.
|
|
2168
|
+
* @throws {AhkoCircuitBreakerOpenError} If the circuit breaker is OPEN and rejects the execution.
|
|
1354
2169
|
*
|
|
1355
2170
|
* @example
|
|
1356
2171
|
* ```typescript
|
|
1357
2172
|
* // Immediate execution (subject to concurrency)
|
|
1358
2173
|
* const count = await ahko.schedule(async () => 42);
|
|
1359
2174
|
*
|
|
1360
|
-
* //
|
|
1361
|
-
* await ahko.schedule(
|
|
1362
|
-
* async ({ signal }) => doWork({ signal }),
|
|
1363
|
-
* { strategy: "delay", delay: 1000 }
|
|
1364
|
-
* );
|
|
2175
|
+
* // High priority task
|
|
2176
|
+
* await ahko.schedule(doUrgentWork, { priority: "high" });
|
|
1365
2177
|
* ```
|
|
1366
2178
|
*/
|
|
1367
2179
|
schedule(task, options) {
|
|
1368
2180
|
if (typeof task !== "function") {
|
|
1369
2181
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1370
2182
|
}
|
|
1371
|
-
const
|
|
2183
|
+
const mergedTags = options?.tags ?? this.defaultScheduleOptions?.tags;
|
|
2184
|
+
const mergedOptions = {
|
|
2185
|
+
...this.defaultScheduleOptions,
|
|
2186
|
+
...options,
|
|
2187
|
+
tags: mergedTags
|
|
2188
|
+
};
|
|
2189
|
+
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1372
2190
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1373
|
-
if (!
|
|
2191
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1374
2192
|
throw new AhkoConfigurationError(
|
|
1375
2193
|
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1376
2194
|
);
|
|
1377
2195
|
}
|
|
1378
|
-
const waitMs =
|
|
2196
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1379
2197
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1380
2198
|
throw new AhkoConfigurationError(
|
|
1381
2199
|
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1382
2200
|
);
|
|
1383
2201
|
}
|
|
1384
2202
|
return this.queue.debounceCoordinator.schedule(
|
|
1385
|
-
|
|
2203
|
+
mergedOptions.key,
|
|
1386
2204
|
task,
|
|
1387
2205
|
waitMs,
|
|
1388
|
-
|
|
2206
|
+
mergedOptions,
|
|
1389
2207
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1390
2208
|
);
|
|
1391
2209
|
}
|
|
1392
2210
|
if (strategy === "throttle" /* THROTTLE */) {
|
|
1393
|
-
if (!
|
|
2211
|
+
if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
|
|
1394
2212
|
throw new AhkoConfigurationError(
|
|
1395
2213
|
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1396
2214
|
);
|
|
1397
2215
|
}
|
|
1398
|
-
const waitMs =
|
|
2216
|
+
const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
|
|
1399
2217
|
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1400
2218
|
throw new AhkoConfigurationError(
|
|
1401
2219
|
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1402
2220
|
);
|
|
1403
2221
|
}
|
|
1404
2222
|
return this.queue.throttleCoordinator.schedule(
|
|
1405
|
-
|
|
2223
|
+
mergedOptions.key,
|
|
1406
2224
|
task,
|
|
1407
2225
|
waitMs,
|
|
1408
|
-
|
|
2226
|
+
mergedOptions,
|
|
1409
2227
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1410
2228
|
);
|
|
1411
2229
|
}
|
|
1412
|
-
const runner = new TaskRunner(
|
|
1413
|
-
|
|
2230
|
+
const runner = new TaskRunner(
|
|
2231
|
+
task,
|
|
2232
|
+
mergedOptions.signal,
|
|
2233
|
+
mergedOptions.timeoutMs,
|
|
2234
|
+
mergedOptions.tags
|
|
2235
|
+
);
|
|
2236
|
+
return this.queue.enqueue(runner, mergedOptions);
|
|
1414
2237
|
}
|
|
1415
2238
|
/**
|
|
1416
2239
|
* Convenience method to schedule a task during platform idle opportunities.
|
|
@@ -1470,15 +2293,185 @@ var Ahko = class {
|
|
|
1470
2293
|
waitMs
|
|
1471
2294
|
});
|
|
1472
2295
|
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Transforms an iterable of items concurrently using an asynchronous mapping function.
|
|
2298
|
+
*
|
|
2299
|
+
* Results are guaranteed to be returned in the original index order.
|
|
2300
|
+
* Concurrency can be capped per-batch or fall back to the scheduler's global limit.
|
|
2301
|
+
*
|
|
2302
|
+
* @template TItem - Type of input elements.
|
|
2303
|
+
* @template TResult - Type of mapped elements.
|
|
2304
|
+
* @param items - Iterable sequence of items to process.
|
|
2305
|
+
* @param fn - Mapper callback receiving item, index, and task context.
|
|
2306
|
+
* @param options - Batch execution options (concurrency, stopOnError, retry, signal, tags, etc.).
|
|
2307
|
+
* @returns Array of transformed results in index order.
|
|
2308
|
+
*
|
|
2309
|
+
* @throws {AhkoConfigurationError} If fn is not a function or concurrency is invalid.
|
|
2310
|
+
* @throws {AhkoCancellationError} If batch or item is cancelled.
|
|
2311
|
+
*
|
|
2312
|
+
* @example
|
|
2313
|
+
* ```typescript
|
|
2314
|
+
* const urls = ["/api/1", "/api/2", "/api/3"];
|
|
2315
|
+
* const data = await ahko.map(urls, async (url, i, { signal }) => {
|
|
2316
|
+
* const res = await fetch(url, { signal });
|
|
2317
|
+
* return res.json();
|
|
2318
|
+
* }, { concurrency: 2 });
|
|
2319
|
+
* ```
|
|
2320
|
+
*/
|
|
2321
|
+
async map(items, fn, options) {
|
|
2322
|
+
if (typeof fn !== "function") {
|
|
2323
|
+
throw new AhkoConfigurationError("Mapper function must be a valid function.");
|
|
2324
|
+
}
|
|
2325
|
+
if (options?.concurrency !== void 0 && (typeof options.concurrency !== "number" || Number.isNaN(options.concurrency) || options.concurrency < 1)) {
|
|
2326
|
+
throw new AhkoConfigurationError(
|
|
2327
|
+
`Invalid concurrency "${options.concurrency}". Must be a number greater than or equal to 1.`
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
const list = Array.from(items);
|
|
2331
|
+
if (list.length === 0) {
|
|
2332
|
+
return [];
|
|
2333
|
+
}
|
|
2334
|
+
const { concurrency, stopOnError = false, signal: externalSignal, ...scheduleOpts } = options ?? {};
|
|
2335
|
+
if (externalSignal?.aborted) {
|
|
2336
|
+
throw new AhkoCancellationError(
|
|
2337
|
+
externalSignal.reason ? `Batch cancelled: ${String(externalSignal.reason)}` : "Batch cancelled"
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const abortController = new AbortController();
|
|
2341
|
+
const results = new Array(list.length);
|
|
2342
|
+
let firstError = void 0;
|
|
2343
|
+
let hasAborted = false;
|
|
2344
|
+
const localizedLimit = concurrency !== void 0 ? Math.floor(concurrency) : Number.isFinite(this.concurrency) ? this.concurrency : Infinity;
|
|
2345
|
+
return new Promise((resolve, reject) => {
|
|
2346
|
+
let currentIndex = 0;
|
|
2347
|
+
let activeCount = 0;
|
|
2348
|
+
let settledCount = 0;
|
|
2349
|
+
const onExternalAbort = () => {
|
|
2350
|
+
const reason = externalSignal?.reason ?? "Batch cancelled by external signal";
|
|
2351
|
+
const err = new AhkoCancellationError(
|
|
2352
|
+
typeof reason === "string" ? reason : "Batch cancelled by external signal"
|
|
2353
|
+
);
|
|
2354
|
+
cleanupAndReject(err);
|
|
2355
|
+
};
|
|
2356
|
+
if (externalSignal) {
|
|
2357
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
2358
|
+
}
|
|
2359
|
+
const cleanupAndReject = (err) => {
|
|
2360
|
+
if (!hasAborted) {
|
|
2361
|
+
hasAborted = true;
|
|
2362
|
+
abortController.abort(err);
|
|
2363
|
+
}
|
|
2364
|
+
if (externalSignal) {
|
|
2365
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2366
|
+
}
|
|
2367
|
+
reject(err);
|
|
2368
|
+
};
|
|
2369
|
+
const checkCompletion = () => {
|
|
2370
|
+
if (settledCount === list.length) {
|
|
2371
|
+
if (externalSignal) {
|
|
2372
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2373
|
+
}
|
|
2374
|
+
if (firstError !== void 0) {
|
|
2375
|
+
reject(firstError);
|
|
2376
|
+
} else {
|
|
2377
|
+
resolve(results);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
const launchNext = () => {
|
|
2382
|
+
if (hasAborted && stopOnError) {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
while (currentIndex < list.length && activeCount < localizedLimit && !(hasAborted && stopOnError)) {
|
|
2386
|
+
const index = currentIndex++;
|
|
2387
|
+
const item = list[index];
|
|
2388
|
+
activeCount++;
|
|
2389
|
+
const taskPromise = this.schedule(
|
|
2390
|
+
(context) => fn(item, index, context),
|
|
2391
|
+
{
|
|
2392
|
+
...scheduleOpts,
|
|
2393
|
+
signal: abortController.signal
|
|
2394
|
+
}
|
|
2395
|
+
);
|
|
2396
|
+
taskPromise.then((result) => {
|
|
2397
|
+
results[index] = result;
|
|
2398
|
+
}).catch((err) => {
|
|
2399
|
+
if (firstError === void 0) {
|
|
2400
|
+
firstError = err;
|
|
2401
|
+
}
|
|
2402
|
+
if (stopOnError && !hasAborted) {
|
|
2403
|
+
cleanupAndReject(err);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
}).finally(() => {
|
|
2407
|
+
activeCount--;
|
|
2408
|
+
settledCount++;
|
|
2409
|
+
if (hasAborted && stopOnError) {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (currentIndex < list.length) {
|
|
2413
|
+
launchNext();
|
|
2414
|
+
} else {
|
|
2415
|
+
checkCompletion();
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
if (abortController.signal.aborted) {
|
|
2421
|
+
cleanupAndReject(abortController.signal.reason);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
launchNext();
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Iterates sequentially or concurrently over an iterable sequence of items,
|
|
2429
|
+
* executing the callback function for each element.
|
|
2430
|
+
*
|
|
2431
|
+
* @template TItem - Type of input elements.
|
|
2432
|
+
* @param items - Iterable sequence of items to process.
|
|
2433
|
+
* @param fn - Callback receiving item, index, and task context.
|
|
2434
|
+
* @param options - Batch execution options.
|
|
2435
|
+
* @returns Promise resolving once all items have finished executing.
|
|
2436
|
+
*
|
|
2437
|
+
* @example
|
|
2438
|
+
* ```typescript
|
|
2439
|
+
* await ahko.each(userQueue, async (user, index, { signal }) => {
|
|
2440
|
+
* await sendWelcomeEmail(user, { signal });
|
|
2441
|
+
* }, { concurrency: 5 });
|
|
2442
|
+
* ```
|
|
2443
|
+
*/
|
|
2444
|
+
async each(items, fn, options) {
|
|
2445
|
+
await this.map(items, fn, options);
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Cancels all pending, delayed, and active tasks tagged with the given tag.
|
|
2449
|
+
*
|
|
2450
|
+
* @param tag - Tag identifier.
|
|
2451
|
+
* @param reason - Optional cancellation reason.
|
|
2452
|
+
* @returns Total number of tasks cancelled.
|
|
2453
|
+
*/
|
|
2454
|
+
cancelByTag(tag, reason) {
|
|
2455
|
+
return this.queue.cancelByTag(tag, reason);
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Retrieves active and pending task counts for a given tag.
|
|
2459
|
+
*
|
|
2460
|
+
* @param tag - Tag identifier.
|
|
2461
|
+
* @returns Object with activeTasks and pendingTasks counts.
|
|
2462
|
+
*/
|
|
2463
|
+
statsByTag(tag) {
|
|
2464
|
+
return this.queue.getStatsByTag(tag);
|
|
2465
|
+
}
|
|
1473
2466
|
/**
|
|
1474
2467
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
1475
2468
|
*
|
|
1476
|
-
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled,
|
|
2469
|
+
* @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, timed out tasks, pause status, and circuit state.
|
|
1477
2470
|
*
|
|
1478
2471
|
* @example
|
|
1479
2472
|
* ```typescript
|
|
1480
2473
|
* const stats = ahko.stats();
|
|
1481
|
-
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
|
|
2474
|
+
* console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
|
|
1482
2475
|
* ```
|
|
1483
2476
|
*/
|
|
1484
2477
|
stats() {
|
|
@@ -1564,7 +2557,7 @@ var Ahko = class {
|
|
|
1564
2557
|
};
|
|
1565
2558
|
|
|
1566
2559
|
// src/version.ts
|
|
1567
|
-
var VERSION = "1.
|
|
2560
|
+
var VERSION = "1.1.5";
|
|
1568
2561
|
|
|
1569
2562
|
// src/errors/queue.error.ts
|
|
1570
2563
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -1636,18 +2629,29 @@ function combineSignals(signals) {
|
|
|
1636
2629
|
};
|
|
1637
2630
|
}
|
|
1638
2631
|
export {
|
|
2632
|
+
AdaptiveCoordinator,
|
|
1639
2633
|
Ahko,
|
|
1640
2634
|
AhkoCancellationError,
|
|
2635
|
+
AhkoCircuitBreakerOpenError,
|
|
1641
2636
|
AhkoConfigurationError,
|
|
1642
2637
|
AhkoError,
|
|
1643
2638
|
AhkoQueueError,
|
|
1644
2639
|
AhkoTimeoutError,
|
|
2640
|
+
CircuitBreakerCoordinator,
|
|
1645
2641
|
DEFAULT_BASE_DELAY,
|
|
1646
2642
|
DEFAULT_MAX_DELAY,
|
|
2643
|
+
ECircuitState,
|
|
1647
2644
|
EScheduleStrategy,
|
|
1648
2645
|
ETaskState,
|
|
2646
|
+
TASK_PRIORITY_WEIGHTS,
|
|
1649
2647
|
VERSION,
|
|
1650
2648
|
calculateBackoff,
|
|
1651
|
-
combineSignals
|
|
2649
|
+
combineSignals,
|
|
2650
|
+
getActiveConfig,
|
|
2651
|
+
getProfileConfig,
|
|
2652
|
+
loadConfig,
|
|
2653
|
+
loadConfigFile,
|
|
2654
|
+
resetConfig,
|
|
2655
|
+
resolvePriorityWeight
|
|
1652
2656
|
};
|
|
1653
2657
|
//# sourceMappingURL=index.js.map
|