@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/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/errors/cancellation.error.ts
98
- var AhkoCancellationError = class extends AhkoError {
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
- * Creates a new AhkoCancellationError.
236
+ * Initializes a new AdaptiveCoordinator instance.
101
237
  *
102
- * @param message - Reason for cancellation.
103
- * @param options - Standard Error options including cause.
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(message = "Task was cancelled", options) {
106
- super(message, options);
107
- this.name = "AhkoCancellationError";
108
- Object.setPrototypeOf(this, new.target.prototype);
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
- concurrency;
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.concurrency = concurrency;
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
- this.cancelledTasks++;
654
- this.emitter.emit("task:cancel", {
655
- taskId: runner.taskId,
656
- reason: "Task cancelled while queued"
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.queue.push(runner);
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
- this.cancelledTasks++;
682
- this.emitter.emit("task:cancel", {
683
- taskId: runner.taskId,
684
- reason: "Task cancelled while queued"
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.queue.push(runner);
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
- this.cancelledTasks++;
699
- this.emitter.emit("task:cancel", {
700
- taskId: runner.taskId,
701
- reason: "Task cancelled while waiting in delay"
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
- this.cancelledTasks++;
723
- this.emitter.emit("task:cancel", {
724
- taskId: runner.taskId,
725
- reason: "Task cancelled while queued"
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.queue.push(runner);
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
- this.cancelledTasks++;
740
- this.emitter.emit("task:cancel", {
741
- taskId: runner.taskId,
742
- reason: "Task cancelled while waiting for idle"
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 and minIntervalMs is respected.
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.concurrency) {
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.concurrency && this.queue.length > 0) {
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.concurrency) {
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
- this.cancelledTasks++;
890
- this.emitter.emit("task:cancel", {
891
- taskId: runner.taskId,
892
- reason: "Task cancelled while queued"
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.queue.push(runner);
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
- this.cancelledTasks++;
913
- this.emitter.emit("task:cancel", {
914
- taskId: runner.taskId,
915
- reason: "Task cancelled while queued"
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.queue.push(runner);
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
- this.cancelledTasks++;
930
- this.emitter.emit("task:cancel", {
931
- taskId: runner.taskId,
932
- reason: "Task cancelled during retry backoff"
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.concurrency
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
- if (this._state === "timed_out" /* TIMED_OUT */) {
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
- this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
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
- * // Delayed execution
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 strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
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 (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
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 = options.waitMs ?? options.delay;
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
- options.key,
2203
+ mergedOptions.key,
1386
2204
  task,
1387
2205
  waitMs,
1388
- options,
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 (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
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 = options.waitMs ?? options.delay;
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
- options.key,
2223
+ mergedOptions.key,
1406
2224
  task,
1407
2225
  waitMs,
1408
- options,
2226
+ mergedOptions,
1409
2227
  (t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
1410
2228
  );
1411
2229
  }
1412
- const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
1413
- return this.queue.enqueue(runner, options);
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, and timed out tasks.
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.0.0";
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