@mrjacket/ahko 1.1.0 → 1.1.6

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
@@ -20,6 +20,21 @@ var AhkoError = class extends Error {
20
20
  }
21
21
  };
22
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
+
23
38
  // src/errors/configuration.error.ts
24
39
  var AhkoConfigurationError = class extends AhkoError {
25
40
  /**
@@ -206,6 +221,137 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
206
221
  return Math.floor(cappedDelay);
207
222
  }
208
223
 
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;
235
+ /**
236
+ * Initializes a new AdaptiveCoordinator instance.
237
+ *
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.
242
+ */
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
+
209
355
  // src/models/circuit-breaker.model.ts
210
356
  var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
211
357
  ECircuitState2["CLOSED"] = "closed";
@@ -323,21 +469,6 @@ var CircuitBreakerCoordinator = class {
323
469
  }
324
470
  };
325
471
 
326
- // src/errors/cancellation.error.ts
327
- var AhkoCancellationError = class extends AhkoError {
328
- /**
329
- * Creates a new AhkoCancellationError.
330
- *
331
- * @param message - Reason for cancellation.
332
- * @param options - Standard Error options including cause.
333
- */
334
- constructor(message = "Task was cancelled", options) {
335
- super(message, options);
336
- this.name = "AhkoCancellationError";
337
- Object.setPrototypeOf(this, new.target.prototype);
338
- }
339
- };
340
-
341
472
  // src/scheduler/debounce-coordinator.ts
342
473
  var DebounceCoordinator = class {
343
474
  entries = /* @__PURE__ */ new Map();
@@ -734,7 +865,7 @@ var AhkoEventEmitter = class {
734
865
  // src/scheduler/task-queue.ts
735
866
  var TaskQueue = class {
736
867
  /** Maximum concurrent active tasks */
737
- concurrency;
868
+ _concurrency;
738
869
  /** Minimum interval in milliseconds between consecutive task starts */
739
870
  minIntervalMs;
740
871
  /** Timestamp of the most recent task start */
@@ -751,6 +882,8 @@ var TaskQueue = class {
751
882
  idleEntries = /* @__PURE__ */ new Set();
752
883
  /** Set of tasks currently awaiting a retry backoff timer */
753
884
  retryEntries = /* @__PURE__ */ new Set();
885
+ /** Tag index for selective cancellation and task classification */
886
+ tagIndex = /* @__PURE__ */ new Map();
754
887
  /** Coordinator for debounced tasks with key coalescing */
755
888
  debounceCoordinator = new DebounceCoordinator();
756
889
  /** Coordinator for throttled tasks with leading/trailing coalescing */
@@ -759,6 +892,8 @@ var TaskQueue = class {
759
892
  emitter = new AhkoEventEmitter();
760
893
  /** Circuit breaker coordinator if configured */
761
894
  circuitBreakerCoordinator;
895
+ /** Adaptive concurrency coordinator if configured */
896
+ adaptiveCoordinator;
762
897
  /** Pause state flag */
763
898
  _isPaused = false;
764
899
  /** Set of pending resolvers awaiting scheduler idle transition */
@@ -783,9 +918,10 @@ var TaskQueue = class {
783
918
  * @param concurrency - Maximum concurrent tasks (defaults to Infinity).
784
919
  * @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
785
920
  * @param circuitBreakerOptions - Optional circuit breaker policy configuration.
921
+ * @param adaptiveOptions - Optional adaptive concurrency policy configuration.
786
922
  * @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
787
923
  */
788
- constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
924
+ constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions, adaptiveOptions) {
789
925
  if (Number.isNaN(concurrency) || concurrency < 1) {
790
926
  throw new AhkoConfigurationError(
791
927
  `Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
@@ -796,14 +932,62 @@ var TaskQueue = class {
796
932
  `Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
797
933
  );
798
934
  }
799
- this.concurrency = concurrency;
935
+ this._concurrency = concurrency;
800
936
  this.minIntervalMs = minIntervalMs;
801
937
  if (circuitBreakerOptions) {
802
938
  this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
803
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
+ }
804
956
  this.debounceCoordinator.onSettled = () => this.checkIdle();
805
957
  this.throttleCoordinator.onSettled = () => this.checkIdle();
806
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
+ }
807
991
  /**
808
992
  * Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
809
993
  */
@@ -825,6 +1009,76 @@ var TaskQueue = class {
825
1009
  isPaused() {
826
1010
  return this._isPaused;
827
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
+ }
828
1082
  /**
829
1083
  * Inserts a task runner into the queue based on priority weight (descending).
830
1084
  * Preserves FIFO ordering among tasks with identical priority.
@@ -906,6 +1160,11 @@ var TaskQueue = class {
906
1160
  if (options) {
907
1161
  this.runnerOptions.set(runner, options);
908
1162
  }
1163
+ this.indexTaskTags(runner);
1164
+ runner.promise.finally(() => {
1165
+ this.cleanupTaskTags(runner);
1166
+ }).catch(() => {
1167
+ });
909
1168
  if (runner.state === "cancelled" /* CANCELLED */) {
910
1169
  this.cancelledTasks++;
911
1170
  return runner.promise;
@@ -1084,7 +1343,7 @@ var TaskQueue = class {
1084
1343
  * and queue is not paused.
1085
1344
  */
1086
1345
  pump() {
1087
- if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
1346
+ if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this._concurrency) {
1088
1347
  return;
1089
1348
  }
1090
1349
  if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
@@ -1101,7 +1360,7 @@ var TaskQueue = class {
1101
1360
  return;
1102
1361
  }
1103
1362
  }
1104
- while (!this._isPaused && this.activeRunners.size < this.concurrency && this.queue.length > 0) {
1363
+ while (!this._isPaused && this.activeRunners.size < this._concurrency && this.queue.length > 0) {
1105
1364
  if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
1106
1365
  const now = Date.now();
1107
1366
  const elapsed = now - this.lastTaskStartTime;
@@ -1143,7 +1402,7 @@ var TaskQueue = class {
1143
1402
  this.lastTaskStartTime = Date.now();
1144
1403
  void this.executeRunner(runner);
1145
1404
  if (this.minIntervalMs > 0) {
1146
- if (this.queue.length > 0 && this.activeRunners.size < this.concurrency) {
1405
+ if (this.queue.length > 0 && this.activeRunners.size < this._concurrency) {
1147
1406
  if (this.rateLimitTimer === void 0) {
1148
1407
  this.rateLimitTimer = setTimeout(() => {
1149
1408
  this.rateLimitTimer = void 0;
@@ -1168,6 +1427,7 @@ var TaskQueue = class {
1168
1427
  try {
1169
1428
  const result = await runner.run();
1170
1429
  this.circuitBreakerCoordinator?.recordSuccess();
1430
+ this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
1171
1431
  this.completedTasks++;
1172
1432
  this.activeRunners.delete(runner);
1173
1433
  this.runnerOptions.delete(runner);
@@ -1206,6 +1466,7 @@ var TaskQueue = class {
1206
1466
  if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
1207
1467
  this.circuitBreakerCoordinator.recordFailure(error);
1208
1468
  }
1469
+ this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
1209
1470
  if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
1210
1471
  this.timedOutTasks++;
1211
1472
  this.emitter.emit("task:timeout", {
@@ -1405,9 +1666,10 @@ var TaskQueue = class {
1405
1666
  timedOutTasks: this.timedOutTasks,
1406
1667
  retriedTasks: this.retriedTasks,
1407
1668
  totalDispatched: this.totalDispatched,
1408
- capacity: this.concurrency,
1669
+ capacity: this._concurrency,
1409
1670
  isPaused: this._isPaused,
1410
- circuitState: this.circuitBreakerCoordinator?.state
1671
+ circuitState: this.circuitBreakerCoordinator?.state,
1672
+ adaptive: this.adaptiveCoordinator?.getStats()
1411
1673
  });
1412
1674
  }
1413
1675
  };
@@ -1443,18 +1705,22 @@ var TaskRunner = class {
1443
1705
  attempt = 1;
1444
1706
  /** Duration of the most recent execution attempt in milliseconds */
1445
1707
  lastDurationMs = 0;
1708
+ /** Set of classification tags associated with this task */
1709
+ tags;
1446
1710
  /**
1447
1711
  * Creates a new TaskRunner instance.
1448
1712
  *
1449
1713
  * @param task - The asynchronous work unit to run.
1450
1714
  * @param externalSignal - Optional external AbortSignal to propagate.
1451
1715
  * @param timeoutMs - Optional maximum execution time in milliseconds.
1716
+ * @param tags - Optional array of tags for classifying and selectively cancelling tasks.
1452
1717
  */
1453
- constructor(task, externalSignal, timeoutMs) {
1718
+ constructor(task, externalSignal, timeoutMs, tags) {
1454
1719
  this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1455
1720
  this.task = task;
1456
1721
  this.externalSignal = externalSignal;
1457
1722
  this.timeoutMs = timeoutMs;
1723
+ this.tags = new Set(tags ?? []);
1458
1724
  this.abortController = new AbortController();
1459
1725
  this.promise = new Promise((resolve, reject) => {
1460
1726
  this.resolvePromise = resolve;
@@ -1781,7 +2047,8 @@ var Ahko = class _Ahko {
1781
2047
  return new _Ahko({
1782
2048
  ...profile,
1783
2049
  ...overrides,
1784
- circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker
2050
+ circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker,
2051
+ adaptive: overrides?.adaptive ?? profile?.adaptive
1785
2052
  });
1786
2053
  }
1787
2054
  /**
@@ -1800,22 +2067,40 @@ var Ahko = class _Ahko {
1800
2067
  const mergedOptions = {
1801
2068
  ...profile,
1802
2069
  ...options,
1803
- circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker
2070
+ circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker,
2071
+ adaptive: options?.adaptive ?? profile?.adaptive
1804
2072
  };
1805
2073
  if (profile) {
1806
2074
  this.defaultScheduleOptions = {
1807
2075
  priority: profile.priority,
1808
2076
  retry: profile.retry,
1809
2077
  timeoutMs: profile.timeoutMs,
1810
- totalTimeoutMs: profile.totalTimeoutMs
2078
+ totalTimeoutMs: profile.totalTimeoutMs,
2079
+ tags: profile.tags
1811
2080
  };
1812
2081
  }
1813
2082
  this.queue = new TaskQueue(
1814
2083
  mergedOptions.concurrency,
1815
2084
  mergedOptions.minIntervalMs,
1816
- mergedOptions.circuitBreaker
2085
+ mergedOptions.circuitBreaker,
2086
+ mergedOptions.adaptive
1817
2087
  );
1818
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
+ }
1819
2104
  /**
1820
2105
  * Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
1821
2106
  */
@@ -1895,9 +2180,11 @@ var Ahko = class _Ahko {
1895
2180
  if (typeof task !== "function") {
1896
2181
  throw new AhkoConfigurationError("Task must be a valid function.");
1897
2182
  }
2183
+ const mergedTags = options?.tags ?? this.defaultScheduleOptions?.tags;
1898
2184
  const mergedOptions = {
1899
2185
  ...this.defaultScheduleOptions,
1900
- ...options
2186
+ ...options,
2187
+ tags: mergedTags
1901
2188
  };
1902
2189
  const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
1903
2190
  if (strategy === "debounce" /* DEBOUNCE */) {
@@ -1940,7 +2227,12 @@ var Ahko = class _Ahko {
1940
2227
  (t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
1941
2228
  );
1942
2229
  }
1943
- const runner = new TaskRunner(task, mergedOptions.signal, mergedOptions.timeoutMs);
2230
+ const runner = new TaskRunner(
2231
+ task,
2232
+ mergedOptions.signal,
2233
+ mergedOptions.timeoutMs,
2234
+ mergedOptions.tags
2235
+ );
1944
2236
  return this.queue.enqueue(runner, mergedOptions);
1945
2237
  }
1946
2238
  /**
@@ -2001,6 +2293,176 @@ var Ahko = class _Ahko {
2001
2293
  waitMs
2002
2294
  });
2003
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
+ }
2004
2466
  /**
2005
2467
  * Retrieves real-time telemetry metrics from the scheduler.
2006
2468
  *
@@ -2095,7 +2557,7 @@ var Ahko = class _Ahko {
2095
2557
  };
2096
2558
 
2097
2559
  // src/version.ts
2098
- var VERSION = "1.1.0";
2560
+ var VERSION = "1.1.6";
2099
2561
 
2100
2562
  // src/errors/queue.error.ts
2101
2563
  var AhkoQueueError = class extends AhkoError {
@@ -2167,6 +2629,7 @@ function combineSignals(signals) {
2167
2629
  };
2168
2630
  }
2169
2631
  export {
2632
+ AdaptiveCoordinator,
2170
2633
  Ahko,
2171
2634
  AhkoCancellationError,
2172
2635
  AhkoCircuitBreakerOpenError,