@mrjacket/ahko 1.1.0 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +90 -1
- package/dist/ahko.d.ts +75 -0
- package/dist/index.cjs +495 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +494 -31
- package/dist/index.js.map +1 -1
- package/dist/models/adaptive.model.d.ts +47 -0
- package/dist/models/batch.model.d.ts +22 -0
- package/dist/models/events.model.d.ts +6 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/options.model.d.ts +9 -0
- package/dist/models/stats.model.d.ts +3 -0
- package/dist/scheduler/adaptive-coordinator.d.ts +45 -0
- package/dist/scheduler/task-queue.d.ts +47 -3
- package/dist/scheduler/task-runner.d.ts +4 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
2560
|
+
var VERSION = "1.1.5";
|
|
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,
|