@mrjacket/ahko 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/errors/ahko.error.ts
2
9
  var AhkoError = class extends Error {
3
10
  /**
@@ -28,6 +35,72 @@ var AhkoConfigurationError = class extends AhkoError {
28
35
  }
29
36
  };
30
37
 
38
+ // src/config/config-loader.ts
39
+ var activeConfig;
40
+ function loadConfig(config) {
41
+ activeConfig = { ...config };
42
+ }
43
+ function resetConfig() {
44
+ activeConfig = void 0;
45
+ }
46
+ async function loadConfigFile(filePath = "config.ahko.json") {
47
+ if (typeof process === "undefined" || !process.versions?.node) {
48
+ return void 0;
49
+ }
50
+ try {
51
+ const { readFile } = await import("fs/promises");
52
+ const { resolve } = await import("path");
53
+ const resolvedPath = resolve(process.cwd(), filePath);
54
+ const content = await readFile(resolvedPath, "utf-8");
55
+ const parsed = JSON.parse(content);
56
+ activeConfig = parsed;
57
+ return parsed;
58
+ } catch {
59
+ return void 0;
60
+ }
61
+ }
62
+ function tryAutoDiscoverSync() {
63
+ if (activeConfig !== void 0 || typeof process === "undefined" || !process.versions?.node) {
64
+ return;
65
+ }
66
+ try {
67
+ let fs = null;
68
+ let path = null;
69
+ if (typeof process.getBuiltinModule === "function") {
70
+ const getBuiltin = process.getBuiltinModule;
71
+ fs = getBuiltin("node:fs");
72
+ path = getBuiltin("node:path");
73
+ } else if (typeof __require === "function") {
74
+ fs = __require("fs");
75
+ path = __require("path");
76
+ }
77
+ if (fs && path) {
78
+ const configPath = path.resolve(process.cwd(), "config.ahko.json");
79
+ if (fs.existsSync(configPath)) {
80
+ const raw = fs.readFileSync(configPath, "utf-8");
81
+ activeConfig = JSON.parse(raw);
82
+ }
83
+ }
84
+ } catch {
85
+ }
86
+ }
87
+ function getActiveConfig() {
88
+ if (activeConfig === void 0) {
89
+ tryAutoDiscoverSync();
90
+ }
91
+ return activeConfig;
92
+ }
93
+ function getProfileConfig(profileName) {
94
+ const config = getActiveConfig();
95
+ if (!config) {
96
+ return void 0;
97
+ }
98
+ if (profileName) {
99
+ return config.profiles?.[profileName];
100
+ }
101
+ return config.default;
102
+ }
103
+
31
104
  // src/models/strategy.model.ts
32
105
  var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
33
106
  EScheduleStrategy2["IMMEDIATE"] = "immediate";
@@ -38,6 +111,23 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
38
111
  return EScheduleStrategy2;
39
112
  })(EScheduleStrategy || {});
40
113
 
114
+ // src/errors/circuit-breaker.error.ts
115
+ var AhkoCircuitBreakerOpenError = class extends AhkoError {
116
+ /** Time remaining in milliseconds before trial execution is allowed */
117
+ resetTimeoutMs;
118
+ /** Timestamp when the circuit tripped open */
119
+ trippedAt;
120
+ /** Total consecutive failures that caused the trip */
121
+ consecutiveFailures;
122
+ constructor(message = "Circuit breaker is open. Fast-failing task execution to protect downstream resources.", options) {
123
+ super(message);
124
+ this.name = "AhkoCircuitBreakerOpenError";
125
+ this.resetTimeoutMs = options?.resetTimeoutMs;
126
+ this.trippedAt = options?.trippedAt;
127
+ this.consecutiveFailures = options?.consecutiveFailures;
128
+ }
129
+ };
130
+
41
131
  // src/errors/timeout.error.ts
42
132
  var AhkoTimeoutError = class extends AhkoError {
43
133
  /**
@@ -58,6 +148,28 @@ var AhkoTimeoutError = class extends AhkoError {
58
148
  }
59
149
  };
60
150
 
151
+ // src/models/priority.model.ts
152
+ var TASK_PRIORITY_WEIGHTS = {
153
+ high: 10,
154
+ normal: 0,
155
+ low: -10
156
+ };
157
+ function resolvePriorityWeight(priority) {
158
+ if (priority === void 0) {
159
+ return TASK_PRIORITY_WEIGHTS.normal;
160
+ }
161
+ if (typeof priority === "number") {
162
+ return Number.isFinite(priority) ? priority : TASK_PRIORITY_WEIGHTS.normal;
163
+ }
164
+ if (priority === "high") {
165
+ return TASK_PRIORITY_WEIGHTS.high;
166
+ }
167
+ if (priority === "low") {
168
+ return TASK_PRIORITY_WEIGHTS.low;
169
+ }
170
+ return TASK_PRIORITY_WEIGHTS.normal;
171
+ }
172
+
61
173
  // src/models/state.model.ts
62
174
  var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
63
175
  ETaskState2["PENDING"] = "pending";
@@ -94,6 +206,123 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
94
206
  return Math.floor(cappedDelay);
95
207
  }
96
208
 
209
+ // src/models/circuit-breaker.model.ts
210
+ var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
211
+ ECircuitState2["CLOSED"] = "closed";
212
+ ECircuitState2["OPEN"] = "open";
213
+ ECircuitState2["HALF_OPEN"] = "half_open";
214
+ return ECircuitState2;
215
+ })(ECircuitState || {});
216
+
217
+ // src/scheduler/circuit-breaker.ts
218
+ var CircuitBreakerCoordinator = class {
219
+ _state = "closed" /* CLOSED */;
220
+ _consecutiveFailures = 0;
221
+ _lastFailureTime;
222
+ failureThreshold;
223
+ resetTimeoutMs;
224
+ /**
225
+ * Initializes a new CircuitBreakerCoordinator.
226
+ *
227
+ * @param options - Configuration options for threshold and cool-down window.
228
+ * @throws {AhkoConfigurationError} If options are invalid.
229
+ */
230
+ constructor(options) {
231
+ if (typeof options.failureThreshold !== "number" || Number.isNaN(options.failureThreshold) || !Number.isInteger(options.failureThreshold) || options.failureThreshold < 1) {
232
+ throw new AhkoConfigurationError(
233
+ `Invalid failureThreshold "${options.failureThreshold}". failureThreshold must be an integer greater than or equal to 1.`
234
+ );
235
+ }
236
+ if (typeof options.resetTimeoutMs !== "number" || Number.isNaN(options.resetTimeoutMs) || !Number.isFinite(options.resetTimeoutMs) || options.resetTimeoutMs <= 0) {
237
+ throw new AhkoConfigurationError(
238
+ `Invalid resetTimeoutMs "${options.resetTimeoutMs}". resetTimeoutMs must be a positive finite number greater than 0.`
239
+ );
240
+ }
241
+ this.failureThreshold = options.failureThreshold;
242
+ this.resetTimeoutMs = options.resetTimeoutMs;
243
+ }
244
+ /** Current state of the circuit breaker */
245
+ get state() {
246
+ this.refreshState();
247
+ return this._state;
248
+ }
249
+ /**
250
+ * Checks whether an execution is currently allowed.
251
+ * If the circuit is OPEN and cool-down has not elapsed, fast-fails immediately.
252
+ *
253
+ * @throws {AhkoCircuitBreakerOpenError} If the circuit is currently OPEN.
254
+ */
255
+ checkAllowed() {
256
+ this.refreshState();
257
+ if (this._state === "open" /* OPEN */) {
258
+ const remainingMs = this._lastFailureTime ? Math.max(0, this.resetTimeoutMs - (Date.now() - this._lastFailureTime)) : this.resetTimeoutMs;
259
+ throw new AhkoCircuitBreakerOpenError(
260
+ `Circuit breaker is open. Fast-failing task execution. Remaining cool-down: ${remainingMs}ms.`,
261
+ {
262
+ resetTimeoutMs: remainingMs,
263
+ trippedAt: this._lastFailureTime,
264
+ consecutiveFailures: this._consecutiveFailures
265
+ }
266
+ );
267
+ }
268
+ }
269
+ /**
270
+ * Records a successful task execution.
271
+ * Heals HALF_OPEN state back to CLOSED and resets consecutive failure counters.
272
+ */
273
+ recordSuccess() {
274
+ this._consecutiveFailures = 0;
275
+ this._state = "closed" /* CLOSED */;
276
+ }
277
+ /**
278
+ * Records a failed task execution.
279
+ * Trips CLOSED to OPEN when threshold is met, or re-trips HALF_OPEN immediately.
280
+ *
281
+ * @param _error - Optional error that caused the failure.
282
+ */
283
+ recordFailure(_error) {
284
+ this._consecutiveFailures++;
285
+ this._lastFailureTime = Date.now();
286
+ if (this._state === "half_open" /* HALF_OPEN */) {
287
+ this._state = "open" /* OPEN */;
288
+ return;
289
+ }
290
+ if (this._consecutiveFailures >= this.failureThreshold) {
291
+ this._state = "open" /* OPEN */;
292
+ }
293
+ }
294
+ /**
295
+ * Evaluates if enough time has passed to transition from OPEN to HALF_OPEN.
296
+ */
297
+ refreshState() {
298
+ if (this._state === "open" /* OPEN */ && this._lastFailureTime !== void 0) {
299
+ const elapsed = Date.now() - this._lastFailureTime;
300
+ if (elapsed >= this.resetTimeoutMs) {
301
+ this._state = "half_open" /* HALF_OPEN */;
302
+ }
303
+ }
304
+ }
305
+ /**
306
+ * Resets the circuit breaker back to initial CLOSED state.
307
+ */
308
+ reset() {
309
+ this._state = "closed" /* CLOSED */;
310
+ this._consecutiveFailures = 0;
311
+ this._lastFailureTime = void 0;
312
+ }
313
+ /**
314
+ * Returns a snapshot of circuit breaker telemetry.
315
+ */
316
+ getStats() {
317
+ this.refreshState();
318
+ return {
319
+ state: this._state,
320
+ consecutiveFailures: this._consecutiveFailures,
321
+ lastFailureTime: this._lastFailureTime
322
+ };
323
+ }
324
+ };
325
+
97
326
  // src/errors/cancellation.error.ts
98
327
  var AhkoCancellationError = class extends AhkoError {
99
328
  /**
@@ -528,6 +757,10 @@ var TaskQueue = class {
528
757
  throttleCoordinator = new ThrottleCoordinator();
529
758
  /** Lifecycle event emitter for task and scheduler events */
530
759
  emitter = new AhkoEventEmitter();
760
+ /** Circuit breaker coordinator if configured */
761
+ circuitBreakerCoordinator;
762
+ /** Pause state flag */
763
+ _isPaused = false;
531
764
  /** Set of pending resolvers awaiting scheduler idle transition */
532
765
  idleResolvers = /* @__PURE__ */ new Set();
533
766
  /** WeakMap associating task runners with their scheduling options */
@@ -549,9 +782,10 @@ var TaskQueue = class {
549
782
  *
550
783
  * @param concurrency - Maximum concurrent tasks (defaults to Infinity).
551
784
  * @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
785
+ * @param circuitBreakerOptions - Optional circuit breaker policy configuration.
552
786
  * @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
553
787
  */
554
- constructor(concurrency = Infinity, minIntervalMs = 0) {
788
+ constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
555
789
  if (Number.isNaN(concurrency) || concurrency < 1) {
556
790
  throw new AhkoConfigurationError(
557
791
  `Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
@@ -564,9 +798,51 @@ var TaskQueue = class {
564
798
  }
565
799
  this.concurrency = concurrency;
566
800
  this.minIntervalMs = minIntervalMs;
801
+ if (circuitBreakerOptions) {
802
+ this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
803
+ }
567
804
  this.debounceCoordinator.onSettled = () => this.checkIdle();
568
805
  this.throttleCoordinator.onSettled = () => this.checkIdle();
569
806
  }
807
+ /**
808
+ * Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
809
+ */
810
+ pause() {
811
+ this._isPaused = true;
812
+ }
813
+ /**
814
+ * Resumes queue execution, immediately dispatching waiting tasks up to available concurrency.
815
+ */
816
+ resume() {
817
+ if (this._isPaused) {
818
+ this._isPaused = false;
819
+ this.pump();
820
+ }
821
+ }
822
+ /**
823
+ * Checks whether the task queue is currently paused.
824
+ */
825
+ isPaused() {
826
+ return this._isPaused;
827
+ }
828
+ /**
829
+ * Inserts a task runner into the queue based on priority weight (descending).
830
+ * Preserves FIFO ordering among tasks with identical priority.
831
+ */
832
+ insertIntoQueue(runner) {
833
+ const options = this.runnerOptions.get(runner);
834
+ const targetWeight = resolvePriorityWeight(options?.priority);
835
+ let insertIndex = this.queue.length;
836
+ for (let i = 0; i < this.queue.length; i++) {
837
+ const existingOptions = this.runnerOptions.get(this.queue[i]);
838
+ const existingWeight = resolvePriorityWeight(existingOptions?.priority);
839
+ if (existingWeight < targetWeight) {
840
+ insertIndex = i;
841
+ break;
842
+ }
843
+ }
844
+ this.queue.splice(insertIndex, 0, runner);
845
+ }
570
846
  /**
571
847
  * Enqueues a task runner according to the specified schedule options.
572
848
  *
@@ -620,6 +896,13 @@ var TaskQueue = class {
620
896
  );
621
897
  }
622
898
  }
899
+ if (options?.totalTimeoutMs !== void 0) {
900
+ if (typeof options.totalTimeoutMs !== "number" || Number.isNaN(options.totalTimeoutMs) || !Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0) {
901
+ throw new AhkoConfigurationError(
902
+ `Invalid totalTimeoutMs "${options.totalTimeoutMs}". totalTimeoutMs must be a positive finite number greater than 0.`
903
+ );
904
+ }
905
+ }
623
906
  if (options) {
624
907
  this.runnerOptions.set(runner, options);
625
908
  }
@@ -627,6 +910,16 @@ var TaskQueue = class {
627
910
  this.cancelledTasks++;
628
911
  return runner.promise;
629
912
  }
913
+ if (options?.totalTimeoutMs !== void 0) {
914
+ const budgetMs = options.totalTimeoutMs;
915
+ const totalTimerId = setTimeout(() => {
916
+ runner.timeout(budgetMs, `Task total execution deadline exceeded after ${budgetMs}ms`);
917
+ }, budgetMs);
918
+ runner.promise.finally(() => {
919
+ clearTimeout(totalTimerId);
920
+ }).catch(() => {
921
+ });
922
+ }
630
923
  if (strategy === "delay" /* DELAY */) {
631
924
  const delayMs = options?.delay ?? 0;
632
925
  if (typeof delayMs !== "number" || Number.isNaN(delayMs) || delayMs < 0) {
@@ -650,15 +943,23 @@ var TaskQueue = class {
650
943
  const index = this.queue.indexOf(runner);
651
944
  if (index !== -1) {
652
945
  this.queue.splice(index, 1);
653
- this.cancelledTasks++;
654
- this.emitter.emit("task:cancel", {
655
- taskId: runner.taskId,
656
- reason: "Task cancelled while queued"
657
- });
946
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
947
+ this.timedOutTasks++;
948
+ this.emitter.emit("task:timeout", {
949
+ taskId: runner.taskId,
950
+ timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
951
+ });
952
+ } else {
953
+ this.cancelledTasks++;
954
+ this.emitter.emit("task:cancel", {
955
+ taskId: runner.taskId,
956
+ reason: "Task cancelled while queued"
957
+ });
958
+ }
658
959
  this.checkIdle();
659
960
  }
660
961
  };
661
- this.queue.push(runner);
962
+ this.insertIntoQueue(runner);
662
963
  this.pump();
663
964
  return runner.promise;
664
965
  }
@@ -671,22 +972,30 @@ var TaskQueue = class {
671
972
  runner,
672
973
  timerId: setTimeout(() => {
673
974
  this.delayedEntries.delete(delayedEntry);
674
- if (runner.state === "cancelled" /* CANCELLED */) {
975
+ if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
675
976
  return;
676
977
  }
677
978
  runner.onCancel = () => {
678
979
  const index = this.queue.indexOf(runner);
679
980
  if (index !== -1) {
680
981
  this.queue.splice(index, 1);
681
- this.cancelledTasks++;
682
- this.emitter.emit("task:cancel", {
683
- taskId: runner.taskId,
684
- reason: "Task cancelled while queued"
685
- });
982
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
983
+ this.timedOutTasks++;
984
+ this.emitter.emit("task:timeout", {
985
+ taskId: runner.taskId,
986
+ timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
987
+ });
988
+ } else {
989
+ this.cancelledTasks++;
990
+ this.emitter.emit("task:cancel", {
991
+ taskId: runner.taskId,
992
+ reason: "Task cancelled while queued"
993
+ });
994
+ }
686
995
  this.checkIdle();
687
996
  }
688
997
  };
689
- this.queue.push(runner);
998
+ this.insertIntoQueue(runner);
690
999
  this.pump();
691
1000
  }, delayMs)
692
1001
  };
@@ -695,11 +1004,19 @@ var TaskQueue = class {
695
1004
  if (this.delayedEntries.has(delayedEntry)) {
696
1005
  clearTimeout(delayedEntry.timerId);
697
1006
  this.delayedEntries.delete(delayedEntry);
698
- this.cancelledTasks++;
699
- this.emitter.emit("task:cancel", {
700
- taskId: runner.taskId,
701
- reason: "Task cancelled while waiting in delay"
702
- });
1007
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1008
+ this.timedOutTasks++;
1009
+ this.emitter.emit("task:timeout", {
1010
+ taskId: runner.taskId,
1011
+ timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
1012
+ });
1013
+ } else {
1014
+ this.cancelledTasks++;
1015
+ this.emitter.emit("task:cancel", {
1016
+ taskId: runner.taskId,
1017
+ reason: "Task cancelled while waiting in delay"
1018
+ });
1019
+ }
703
1020
  this.checkIdle();
704
1021
  }
705
1022
  };
@@ -712,22 +1029,30 @@ var TaskQueue = class {
712
1029
  let idleEntry;
713
1030
  const handle = IdleScheduler.schedule(() => {
714
1031
  this.idleEntries.delete(idleEntry);
715
- if (runner.state === "cancelled" /* CANCELLED */) {
1032
+ if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
716
1033
  return;
717
1034
  }
718
1035
  runner.onCancel = () => {
719
1036
  const index = this.queue.indexOf(runner);
720
1037
  if (index !== -1) {
721
1038
  this.queue.splice(index, 1);
722
- this.cancelledTasks++;
723
- this.emitter.emit("task:cancel", {
724
- taskId: runner.taskId,
725
- reason: "Task cancelled while queued"
726
- });
1039
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1040
+ this.timedOutTasks++;
1041
+ this.emitter.emit("task:timeout", {
1042
+ taskId: runner.taskId,
1043
+ timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
1044
+ });
1045
+ } else {
1046
+ this.cancelledTasks++;
1047
+ this.emitter.emit("task:cancel", {
1048
+ taskId: runner.taskId,
1049
+ reason: "Task cancelled while queued"
1050
+ });
1051
+ }
727
1052
  this.checkIdle();
728
1053
  }
729
1054
  };
730
- this.queue.push(runner);
1055
+ this.insertIntoQueue(runner);
731
1056
  this.pump();
732
1057
  }, idleTimeout);
733
1058
  idleEntry = { runner, handle };
@@ -736,21 +1061,30 @@ var TaskQueue = class {
736
1061
  if (this.idleEntries.has(idleEntry)) {
737
1062
  handle.cancel();
738
1063
  this.idleEntries.delete(idleEntry);
739
- this.cancelledTasks++;
740
- this.emitter.emit("task:cancel", {
741
- taskId: runner.taskId,
742
- reason: "Task cancelled while waiting for idle"
743
- });
1064
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1065
+ this.timedOutTasks++;
1066
+ this.emitter.emit("task:timeout", {
1067
+ taskId: runner.taskId,
1068
+ timeoutMs: this.runnerOptions.get(runner)?.totalTimeoutMs ?? runner.timeoutMs
1069
+ });
1070
+ } else {
1071
+ this.cancelledTasks++;
1072
+ this.emitter.emit("task:cancel", {
1073
+ taskId: runner.taskId,
1074
+ reason: "Task cancelled while waiting for idle"
1075
+ });
1076
+ }
744
1077
  this.checkIdle();
745
1078
  }
746
1079
  };
747
1080
  }
748
1081
  /**
749
1082
  * Pumps the queue by picking pending tasks and executing them
750
- * as long as concurrency capacity is available and minIntervalMs is respected.
1083
+ * as long as concurrency capacity is available, minIntervalMs is respected,
1084
+ * and queue is not paused.
751
1085
  */
752
1086
  pump() {
753
- if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
1087
+ if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
754
1088
  return;
755
1089
  }
756
1090
  if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
@@ -767,7 +1101,7 @@ var TaskQueue = class {
767
1101
  return;
768
1102
  }
769
1103
  }
770
- while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
1104
+ while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
771
1105
  if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
772
1106
  const now = Date.now();
773
1107
  const elapsed = now - this.lastTaskStartTime;
@@ -786,9 +1120,25 @@ var TaskQueue = class {
786
1120
  if (!runner) {
787
1121
  break;
788
1122
  }
789
- if (runner.state === "cancelled" /* CANCELLED */) {
1123
+ if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
790
1124
  continue;
791
1125
  }
1126
+ if (this.circuitBreakerCoordinator) {
1127
+ try {
1128
+ this.circuitBreakerCoordinator.checkAllowed();
1129
+ } catch (cbError) {
1130
+ this.failedTasks++;
1131
+ this.runnerOptions.delete(runner);
1132
+ this.emitter.emit("task:fail", {
1133
+ taskId: runner.taskId,
1134
+ attempt: runner.attempt,
1135
+ error: cbError,
1136
+ willRetry: false
1137
+ });
1138
+ runner.reject(cbError);
1139
+ continue;
1140
+ }
1141
+ }
792
1142
  this.activeRunners.add(runner);
793
1143
  this.lastTaskStartTime = Date.now();
794
1144
  void this.executeRunner(runner);
@@ -817,6 +1167,7 @@ var TaskQueue = class {
817
1167
  });
818
1168
  try {
819
1169
  const result = await runner.run();
1170
+ this.circuitBreakerCoordinator?.recordSuccess();
820
1171
  this.completedTasks++;
821
1172
  this.activeRunners.delete(runner);
822
1173
  this.runnerOptions.delete(runner);
@@ -852,11 +1203,14 @@ var TaskQueue = class {
852
1203
  this.scheduleRetry(runner, options);
853
1204
  return;
854
1205
  }
1206
+ if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
1207
+ this.circuitBreakerCoordinator.recordFailure(error);
1208
+ }
855
1209
  if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
856
1210
  this.timedOutTasks++;
857
1211
  this.emitter.emit("task:timeout", {
858
1212
  taskId: runner.taskId,
859
- timeoutMs: runner.timeoutMs
1213
+ timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
860
1214
  });
861
1215
  } else {
862
1216
  this.failedTasks++;
@@ -886,15 +1240,23 @@ var TaskQueue = class {
886
1240
  const index = this.queue.indexOf(runner);
887
1241
  if (index !== -1) {
888
1242
  this.queue.splice(index, 1);
889
- this.cancelledTasks++;
890
- this.emitter.emit("task:cancel", {
891
- taskId: runner.taskId,
892
- reason: "Task cancelled while queued"
893
- });
1243
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1244
+ this.timedOutTasks++;
1245
+ this.emitter.emit("task:timeout", {
1246
+ taskId: runner.taskId,
1247
+ timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
1248
+ });
1249
+ } else {
1250
+ this.cancelledTasks++;
1251
+ this.emitter.emit("task:cancel", {
1252
+ taskId: runner.taskId,
1253
+ reason: "Task cancelled while queued"
1254
+ });
1255
+ }
894
1256
  this.checkIdle();
895
1257
  }
896
1258
  };
897
- this.queue.push(runner);
1259
+ this.insertIntoQueue(runner);
898
1260
  this.pump();
899
1261
  return;
900
1262
  }
@@ -902,22 +1264,30 @@ var TaskQueue = class {
902
1264
  runner,
903
1265
  timerId: setTimeout(() => {
904
1266
  this.retryEntries.delete(retryEntry);
905
- if (runner.state === "cancelled" /* CANCELLED */) {
1267
+ if (runner.state === "cancelled" /* CANCELLED */ || runner.state === "timed_out" /* TIMED_OUT */) {
906
1268
  return;
907
1269
  }
908
1270
  runner.onCancel = () => {
909
1271
  const index = this.queue.indexOf(runner);
910
1272
  if (index !== -1) {
911
1273
  this.queue.splice(index, 1);
912
- this.cancelledTasks++;
913
- this.emitter.emit("task:cancel", {
914
- taskId: runner.taskId,
915
- reason: "Task cancelled while queued"
916
- });
1274
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1275
+ this.timedOutTasks++;
1276
+ this.emitter.emit("task:timeout", {
1277
+ taskId: runner.taskId,
1278
+ timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
1279
+ });
1280
+ } else {
1281
+ this.cancelledTasks++;
1282
+ this.emitter.emit("task:cancel", {
1283
+ taskId: runner.taskId,
1284
+ reason: "Task cancelled while queued"
1285
+ });
1286
+ }
917
1287
  this.checkIdle();
918
1288
  }
919
1289
  };
920
- this.queue.push(runner);
1290
+ this.insertIntoQueue(runner);
921
1291
  this.pump();
922
1292
  }, backoffDelay)
923
1293
  };
@@ -926,11 +1296,19 @@ var TaskQueue = class {
926
1296
  if (this.retryEntries.has(retryEntry)) {
927
1297
  clearTimeout(retryEntry.timerId);
928
1298
  this.retryEntries.delete(retryEntry);
929
- this.cancelledTasks++;
930
- this.emitter.emit("task:cancel", {
931
- taskId: runner.taskId,
932
- reason: "Task cancelled during retry backoff"
933
- });
1299
+ if (runner.totalTimedOut || runner.state === "timed_out" /* TIMED_OUT */) {
1300
+ this.timedOutTasks++;
1301
+ this.emitter.emit("task:timeout", {
1302
+ taskId: runner.taskId,
1303
+ timeoutMs: options?.totalTimeoutMs ?? runner.timeoutMs
1304
+ });
1305
+ } else {
1306
+ this.cancelledTasks++;
1307
+ this.emitter.emit("task:cancel", {
1308
+ taskId: runner.taskId,
1309
+ reason: "Task cancelled during retry backoff"
1310
+ });
1311
+ }
934
1312
  this.checkIdle();
935
1313
  }
936
1314
  };
@@ -977,7 +1355,7 @@ var TaskQueue = class {
977
1355
  clear() {
978
1356
  while (this.queue.length > 0) {
979
1357
  const runner = this.queue.shift();
980
- if (runner && runner.state !== "cancelled" /* CANCELLED */) {
1358
+ if (runner && runner.state !== "cancelled" /* CANCELLED */ && runner.state !== "timed_out" /* TIMED_OUT */) {
981
1359
  runner.cancel("Scheduler cleared");
982
1360
  this.cancelledTasks++;
983
1361
  this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
@@ -1027,7 +1405,9 @@ var TaskQueue = class {
1027
1405
  timedOutTasks: this.timedOutTasks,
1028
1406
  retriedTasks: this.retriedTasks,
1029
1407
  totalDispatched: this.totalDispatched,
1030
- capacity: this.concurrency
1408
+ capacity: this.concurrency,
1409
+ isPaused: this._isPaused,
1410
+ circuitState: this.circuitBreakerCoordinator?.state
1031
1411
  });
1032
1412
  }
1033
1413
  };
@@ -1098,6 +1478,8 @@ var TaskRunner = class {
1098
1478
  }
1099
1479
  }
1100
1480
  }
1481
+ /** Flag indicating if runner was aborted by an overall total timeout deadline */
1482
+ totalTimedOut = false;
1101
1483
  /**
1102
1484
  * Gets the current lifecycle state of the task.
1103
1485
  */
@@ -1130,7 +1512,7 @@ var TaskRunner = class {
1130
1512
  * @returns A promise resolving to true if retry should proceed, false otherwise.
1131
1513
  */
1132
1514
  async canRetry(error, retryOptions) {
1133
- if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
1515
+ if (this.totalTimedOut || this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
1134
1516
  return false;
1135
1517
  }
1136
1518
  if (!retryOptions || typeof retryOptions.attempts !== "number") {
@@ -1171,15 +1553,16 @@ var TaskRunner = class {
1171
1553
  let abortListener;
1172
1554
  const abortPromise = new Promise((_, reject) => {
1173
1555
  abortListener = () => {
1174
- if (this._state === "timed_out" /* TIMED_OUT */) {
1556
+ const reason = this.abortController.signal.reason;
1557
+ if (this._state === "timed_out" /* TIMED_OUT */ || reason instanceof AhkoTimeoutError) {
1558
+ this._state = "timed_out" /* TIMED_OUT */;
1175
1559
  reject(
1176
- new AhkoTimeoutError(
1560
+ reason instanceof AhkoTimeoutError ? reason : new AhkoTimeoutError(
1177
1561
  `Task execution timed out after ${this.timeoutMs}ms`,
1178
1562
  { timeoutMs: this.timeoutMs }
1179
1563
  )
1180
1564
  );
1181
1565
  } else {
1182
- const reason = this.abortController.signal.reason;
1183
1566
  reject(
1184
1567
  new AhkoCancellationError("Task was cancelled during execution", {
1185
1568
  cause: reason instanceof Error ? reason : void 0
@@ -1214,6 +1597,10 @@ var TaskRunner = class {
1214
1597
  }
1215
1598
  taskExecutionPromise.catch(() => {
1216
1599
  });
1600
+ abortPromise.catch(() => {
1601
+ });
1602
+ timeoutPromise?.catch(() => {
1603
+ });
1217
1604
  const racePromises = [
1218
1605
  taskExecutionPromise,
1219
1606
  abortPromise
@@ -1305,6 +1692,31 @@ var TaskRunner = class {
1305
1692
  this.onCancel?.(this);
1306
1693
  }
1307
1694
  }
1695
+ /**
1696
+ * Times out the task, aborting pending or running execution with AhkoTimeoutError.
1697
+ *
1698
+ * @param timeoutMs - Timeout duration in milliseconds.
1699
+ * @param message - Optional custom timeout message.
1700
+ */
1701
+ timeout(timeoutMs, message) {
1702
+ if (this._state === "completed" /* COMPLETED */ || this._state === "failed" /* FAILED */ || this._state === "cancelled" /* CANCELLED */ || this._state === "timed_out" /* TIMED_OUT */) {
1703
+ return;
1704
+ }
1705
+ const wasPending = this._state === "pending" /* PENDING */;
1706
+ this._state = "timed_out" /* TIMED_OUT */;
1707
+ this.totalTimedOut = true;
1708
+ this.clearTimeoutTimer();
1709
+ const timeoutError = new AhkoTimeoutError(
1710
+ message ?? `Task execution timed out after ${timeoutMs}ms`,
1711
+ { timeoutMs }
1712
+ );
1713
+ this.abortController.abort(timeoutError);
1714
+ this.cleanup();
1715
+ if (wasPending) {
1716
+ this.rejectPromise(timeoutError);
1717
+ this.onCancel?.(this);
1718
+ }
1719
+ }
1308
1720
  /**
1309
1721
  * Handles external AbortSignal trigger.
1310
1722
  */
@@ -1323,9 +1735,55 @@ var TaskRunner = class {
1323
1735
  };
1324
1736
 
1325
1737
  // src/ahko.ts
1326
- var Ahko = class {
1738
+ var Ahko = class _Ahko {
1327
1739
  /** Internal queue and concurrency manager */
1328
1740
  queue;
1741
+ /** Default schedule options inherited from profile if configured */
1742
+ defaultScheduleOptions;
1743
+ /**
1744
+ * Programmatically loads a declarative configuration into memory.
1745
+ * Works universally across Node.js, browsers, and edge runtimes.
1746
+ *
1747
+ * @param config - File configuration object containing default and named profiles.
1748
+ */
1749
+ static loadConfig(config) {
1750
+ loadConfig(config);
1751
+ }
1752
+ /**
1753
+ * Asynchronously loads a configuration file from disk (Node.js).
1754
+ *
1755
+ * @param filePath - Path to configuration file (default: "config.ahko.json").
1756
+ */
1757
+ static async loadConfigFile(filePath) {
1758
+ return loadConfigFile(filePath);
1759
+ }
1760
+ /**
1761
+ * Resets the active declarative configuration.
1762
+ */
1763
+ static resetConfig() {
1764
+ resetConfig();
1765
+ }
1766
+ /**
1767
+ * Retrieves the currently active declarative configuration.
1768
+ */
1769
+ static getActiveConfig() {
1770
+ return getActiveConfig();
1771
+ }
1772
+ /**
1773
+ * Instantiates an Ahko scheduler initialized with settings from a declarative profile.
1774
+ *
1775
+ * @param profileName - Optional name of the profile (e.g. "api", "background").
1776
+ * @param overrides - Optional scheduler options overriding profile values.
1777
+ * @returns A new configured Ahko instance.
1778
+ */
1779
+ static fromProfile(profileName, overrides) {
1780
+ const profile = getProfileConfig(profileName);
1781
+ return new _Ahko({
1782
+ ...profile,
1783
+ ...overrides,
1784
+ circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker
1785
+ });
1786
+ }
1329
1787
  /**
1330
1788
  * Initializes a new Ahko scheduler instance.
1331
1789
  *
@@ -1338,79 +1796,152 @@ var Ahko = class {
1338
1796
  * ```
1339
1797
  */
1340
1798
  constructor(options) {
1341
- this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
1799
+ const profile = options?.profile ? getProfileConfig(options.profile) : getProfileConfig();
1800
+ const mergedOptions = {
1801
+ ...profile,
1802
+ ...options,
1803
+ circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker
1804
+ };
1805
+ if (profile) {
1806
+ this.defaultScheduleOptions = {
1807
+ priority: profile.priority,
1808
+ retry: profile.retry,
1809
+ timeoutMs: profile.timeoutMs,
1810
+ totalTimeoutMs: profile.totalTimeoutMs
1811
+ };
1812
+ }
1813
+ this.queue = new TaskQueue(
1814
+ mergedOptions.concurrency,
1815
+ mergedOptions.minIntervalMs,
1816
+ mergedOptions.circuitBreaker
1817
+ );
1818
+ }
1819
+ /**
1820
+ * Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
1821
+ */
1822
+ pause() {
1823
+ this.queue.pause();
1824
+ }
1825
+ /**
1826
+ * Resumes scheduler dispatch, immediately executing waiting tasks up to available concurrency.
1827
+ */
1828
+ resume() {
1829
+ this.queue.resume();
1830
+ }
1831
+ /**
1832
+ * Checks whether the scheduler is currently paused.
1833
+ */
1834
+ isPaused() {
1835
+ return this.queue.isPaused();
1836
+ }
1837
+ /**
1838
+ * Current circuit breaker state if circuit breaker protection is configured.
1839
+ */
1840
+ get circuitState() {
1841
+ return this.queue.circuitBreakerCoordinator?.state;
1842
+ }
1843
+ /**
1844
+ * Access to the underlying circuit breaker coordinator instance if configured.
1845
+ */
1846
+ get circuitBreaker() {
1847
+ return this.queue.circuitBreakerCoordinator;
1848
+ }
1849
+ /**
1850
+ * Wraps an async function so every execution is automatically routed through this Ahko scheduler.
1851
+ *
1852
+ * @template TArgs - Parameter types of the wrapped function.
1853
+ * @template TReturn - Return type of the wrapped function.
1854
+ * @param fn - The function to wrap.
1855
+ * @param options - Optional scheduling options applied to every wrapped call.
1856
+ * @returns A wrapped function returning a Promise.
1857
+ *
1858
+ * @example
1859
+ * ```typescript
1860
+ * const fetchUser = ahko.wrap(async (id: string) => api.getUser(id), { priority: "high" });
1861
+ * const user = await fetchUser("usr_123");
1862
+ * ```
1863
+ */
1864
+ wrap(fn, options) {
1865
+ if (typeof fn !== "function") {
1866
+ throw new AhkoConfigurationError("Target to wrap must be a valid function.");
1867
+ }
1868
+ return (...args) => {
1869
+ return this.schedule(() => fn(...args), options);
1870
+ };
1342
1871
  }
1343
1872
  /**
1344
1873
  * Schedules a task for execution with full return type inference.
1345
1874
  *
1346
1875
  * @template T - Inferred return type of the task.
1347
1876
  * @param task - Asynchronous or synchronous task function accepting an {@link ITaskContext}.
1348
- * @param options - Task-specific scheduling options such as strategy, delay, and cancellation signal.
1877
+ * @param options - Task-specific scheduling options such as strategy, priority, delay, and cancellation signal.
1349
1878
  * @returns A promise that resolves with the task's return value.
1350
1879
  *
1351
1880
  * @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
1352
1881
  * @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
1353
- * @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
1882
+ * @throws {AhkoTimeoutError} If task execution exceeds timeoutMs or totalTimeoutMs.
1883
+ * @throws {AhkoCircuitBreakerOpenError} If the circuit breaker is OPEN and rejects the execution.
1354
1884
  *
1355
1885
  * @example
1356
1886
  * ```typescript
1357
1887
  * // Immediate execution (subject to concurrency)
1358
1888
  * const count = await ahko.schedule(async () => 42);
1359
1889
  *
1360
- * // Delayed execution
1361
- * await ahko.schedule(
1362
- * async ({ signal }) => doWork({ signal }),
1363
- * { strategy: "delay", delay: 1000 }
1364
- * );
1890
+ * // High priority task
1891
+ * await ahko.schedule(doUrgentWork, { priority: "high" });
1365
1892
  * ```
1366
1893
  */
1367
1894
  schedule(task, options) {
1368
1895
  if (typeof task !== "function") {
1369
1896
  throw new AhkoConfigurationError("Task must be a valid function.");
1370
1897
  }
1371
- const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
1898
+ const mergedOptions = {
1899
+ ...this.defaultScheduleOptions,
1900
+ ...options
1901
+ };
1902
+ const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
1372
1903
  if (strategy === "debounce" /* DEBOUNCE */) {
1373
- if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
1904
+ if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
1374
1905
  throw new AhkoConfigurationError(
1375
1906
  `Strategy "debounce" requires a valid "key" of type string or symbol.`
1376
1907
  );
1377
1908
  }
1378
- const waitMs = options.waitMs ?? options.delay;
1909
+ const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
1379
1910
  if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
1380
1911
  throw new AhkoConfigurationError(
1381
1912
  `Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
1382
1913
  );
1383
1914
  }
1384
1915
  return this.queue.debounceCoordinator.schedule(
1385
- options.key,
1916
+ mergedOptions.key,
1386
1917
  task,
1387
1918
  waitMs,
1388
- options,
1919
+ mergedOptions,
1389
1920
  (t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
1390
1921
  );
1391
1922
  }
1392
1923
  if (strategy === "throttle" /* THROTTLE */) {
1393
- if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
1924
+ if (!mergedOptions.key || typeof mergedOptions.key !== "string" && typeof mergedOptions.key !== "symbol") {
1394
1925
  throw new AhkoConfigurationError(
1395
1926
  `Strategy "throttle" requires a valid "key" of type string or symbol.`
1396
1927
  );
1397
1928
  }
1398
- const waitMs = options.waitMs ?? options.delay;
1929
+ const waitMs = mergedOptions.waitMs ?? mergedOptions.delay;
1399
1930
  if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
1400
1931
  throw new AhkoConfigurationError(
1401
1932
  `Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
1402
1933
  );
1403
1934
  }
1404
1935
  return this.queue.throttleCoordinator.schedule(
1405
- options.key,
1936
+ mergedOptions.key,
1406
1937
  task,
1407
1938
  waitMs,
1408
- options,
1939
+ mergedOptions,
1409
1940
  (t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
1410
1941
  );
1411
1942
  }
1412
- const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
1413
- return this.queue.enqueue(runner, options);
1943
+ const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
1944
+ return this.queue.enqueue(runner, mergedOptions);
1414
1945
  }
1415
1946
  /**
1416
1947
  * Convenience method to schedule a task during platform idle opportunities.
@@ -1473,12 +2004,12 @@ var Ahko = class {
1473
2004
  /**
1474
2005
  * Retrieves real-time telemetry metrics from the scheduler.
1475
2006
  *
1476
- * @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, and timed out tasks.
2007
+ * @returns An {@link IAhkoStats} snapshot of active, pending, completed, failed, cancelled, timed out tasks, pause status, and circuit state.
1477
2008
  *
1478
2009
  * @example
1479
2010
  * ```typescript
1480
2011
  * const stats = ahko.stats();
1481
- * console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}`);
2012
+ * console.log(`Active: ${stats.activeTasks}, Pending: ${stats.pendingTasks}, Paused: ${stats.isPaused}`);
1482
2013
  * ```
1483
2014
  */
1484
2015
  stats() {
@@ -1564,7 +2095,7 @@ var Ahko = class {
1564
2095
  };
1565
2096
 
1566
2097
  // src/version.ts
1567
- var VERSION = "1.0.0";
2098
+ var VERSION = "1.1.0";
1568
2099
 
1569
2100
  // src/errors/queue.error.ts
1570
2101
  var AhkoQueueError = class extends AhkoError {
@@ -1638,16 +2169,26 @@ function combineSignals(signals) {
1638
2169
  export {
1639
2170
  Ahko,
1640
2171
  AhkoCancellationError,
2172
+ AhkoCircuitBreakerOpenError,
1641
2173
  AhkoConfigurationError,
1642
2174
  AhkoError,
1643
2175
  AhkoQueueError,
1644
2176
  AhkoTimeoutError,
2177
+ CircuitBreakerCoordinator,
1645
2178
  DEFAULT_BASE_DELAY,
1646
2179
  DEFAULT_MAX_DELAY,
2180
+ ECircuitState,
1647
2181
  EScheduleStrategy,
1648
2182
  ETaskState,
2183
+ TASK_PRIORITY_WEIGHTS,
1649
2184
  VERSION,
1650
2185
  calculateBackoff,
1651
- combineSignals
2186
+ combineSignals,
2187
+ getActiveConfig,
2188
+ getProfileConfig,
2189
+ loadConfig,
2190
+ loadConfigFile,
2191
+ resetConfig,
2192
+ resolvePriorityWeight
1652
2193
  };
1653
2194
  //# sourceMappingURL=index.js.map