@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/CHANGELOG.md +36 -0
- package/README.md +103 -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.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
AdaptiveCoordinator: () => AdaptiveCoordinator,
|
|
33
34
|
Ahko: () => Ahko,
|
|
34
35
|
AhkoCancellationError: () => AhkoCancellationError,
|
|
35
36
|
AhkoCircuitBreakerOpenError: () => AhkoCircuitBreakerOpenError,
|
|
@@ -71,6 +72,21 @@ var AhkoError = class extends Error {
|
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
// src/errors/cancellation.error.ts
|
|
76
|
+
var AhkoCancellationError = class extends AhkoError {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new AhkoCancellationError.
|
|
79
|
+
*
|
|
80
|
+
* @param message - Reason for cancellation.
|
|
81
|
+
* @param options - Standard Error options including cause.
|
|
82
|
+
*/
|
|
83
|
+
constructor(message = "Task was cancelled", options) {
|
|
84
|
+
super(message, options);
|
|
85
|
+
this.name = "AhkoCancellationError";
|
|
86
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
74
90
|
// src/errors/configuration.error.ts
|
|
75
91
|
var AhkoConfigurationError = class extends AhkoError {
|
|
76
92
|
/**
|
|
@@ -257,6 +273,137 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
257
273
|
return Math.floor(cappedDelay);
|
|
258
274
|
}
|
|
259
275
|
|
|
276
|
+
// src/scheduler/adaptive-coordinator.ts
|
|
277
|
+
var AdaptiveCoordinator = class {
|
|
278
|
+
_currentConcurrency;
|
|
279
|
+
minConcurrency;
|
|
280
|
+
maxConcurrency;
|
|
281
|
+
targetLatencyMs;
|
|
282
|
+
sampleWindowSize;
|
|
283
|
+
backoffFactor;
|
|
284
|
+
recentDurations = [];
|
|
285
|
+
lastAverageLatencyMs = 0;
|
|
286
|
+
onConcurrencyChange;
|
|
287
|
+
/**
|
|
288
|
+
* Initializes a new AdaptiveCoordinator instance.
|
|
289
|
+
*
|
|
290
|
+
* @param options - Adaptive concurrency configuration options.
|
|
291
|
+
* @param initialConcurrency - Starting scheduler concurrency limit.
|
|
292
|
+
* @param onConcurrencyChange - Callback invoked when concurrency changes.
|
|
293
|
+
* @throws {AhkoConfigurationError} If options are invalid.
|
|
294
|
+
*/
|
|
295
|
+
constructor(options, initialConcurrency, onConcurrencyChange) {
|
|
296
|
+
if (typeof options.targetLatencyMs !== "number" || Number.isNaN(options.targetLatencyMs) || !Number.isFinite(options.targetLatencyMs) || options.targetLatencyMs <= 0) {
|
|
297
|
+
throw new AhkoConfigurationError(
|
|
298
|
+
`Invalid targetLatencyMs "${options.targetLatencyMs}". targetLatencyMs must be a positive number greater than 0.`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const min = options.minConcurrency ?? 1;
|
|
302
|
+
if (typeof min !== "number" || Number.isNaN(min) || min < 1 || !Number.isInteger(min)) {
|
|
303
|
+
throw new AhkoConfigurationError(
|
|
304
|
+
`Invalid minConcurrency "${min}". minConcurrency must be an integer greater than or equal to 1.`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const defaultMax = Number.isFinite(initialConcurrency) ? Math.max(min, initialConcurrency * 2) : Math.max(min, 10);
|
|
308
|
+
const max = options.maxConcurrency ?? defaultMax;
|
|
309
|
+
if (typeof max !== "number" || Number.isNaN(max) || max < min || !Number.isInteger(max)) {
|
|
310
|
+
throw new AhkoConfigurationError(
|
|
311
|
+
`Invalid maxConcurrency "${max}". maxConcurrency must be an integer greater than or equal to minConcurrency (${min}).`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const windowSize = options.sampleWindowSize ?? 5;
|
|
315
|
+
if (typeof windowSize !== "number" || Number.isNaN(windowSize) || windowSize < 1 || !Number.isInteger(windowSize)) {
|
|
316
|
+
throw new AhkoConfigurationError(
|
|
317
|
+
`Invalid sampleWindowSize "${windowSize}". sampleWindowSize must be an integer greater than or equal to 1.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const factor = options.backoffFactor ?? 0.7;
|
|
321
|
+
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0.1 || factor >= 0.99) {
|
|
322
|
+
throw new AhkoConfigurationError(
|
|
323
|
+
`Invalid backoffFactor "${factor}". backoffFactor must be a number between 0.1 and 0.99.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
this.minConcurrency = min;
|
|
327
|
+
this.maxConcurrency = max;
|
|
328
|
+
this.targetLatencyMs = options.targetLatencyMs;
|
|
329
|
+
this.sampleWindowSize = windowSize;
|
|
330
|
+
this.backoffFactor = factor;
|
|
331
|
+
this.onConcurrencyChange = onConcurrencyChange;
|
|
332
|
+
const clampedInitial = Number.isFinite(initialConcurrency) ? Math.min(Math.max(initialConcurrency, min), max) : min;
|
|
333
|
+
this._currentConcurrency = clampedInitial;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Current effective concurrency limit dictated by the adaptive controller.
|
|
337
|
+
*/
|
|
338
|
+
get currentConcurrency() {
|
|
339
|
+
return this._currentConcurrency;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Manually overrides the current concurrency within [minConcurrency, maxConcurrency].
|
|
343
|
+
*
|
|
344
|
+
* @param concurrency - New concurrency limit to set.
|
|
345
|
+
*/
|
|
346
|
+
setConcurrency(concurrency) {
|
|
347
|
+
const clamped = Math.min(Math.max(concurrency, this.minConcurrency), this.maxConcurrency);
|
|
348
|
+
if (clamped !== this._currentConcurrency) {
|
|
349
|
+
const prev = this._currentConcurrency;
|
|
350
|
+
this._currentConcurrency = clamped;
|
|
351
|
+
this.onConcurrencyChange(prev, clamped, "Manual concurrency override");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Records a task execution duration sample and triggers AIMD adjustment if window is filled.
|
|
356
|
+
*
|
|
357
|
+
* @param durationMs - Execution duration in milliseconds of the completed task.
|
|
358
|
+
*/
|
|
359
|
+
recordDuration(durationMs) {
|
|
360
|
+
this.recentDurations.push(durationMs);
|
|
361
|
+
if (this.recentDurations.length < this.sampleWindowSize) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const total = this.recentDurations.reduce((sum, val) => sum + val, 0);
|
|
365
|
+
const average = total / this.recentDurations.length;
|
|
366
|
+
this.lastAverageLatencyMs = average;
|
|
367
|
+
this.recentDurations = [];
|
|
368
|
+
if (average > this.targetLatencyMs) {
|
|
369
|
+
const decreased = Math.max(
|
|
370
|
+
this.minConcurrency,
|
|
371
|
+
Math.floor(this._currentConcurrency * this.backoffFactor)
|
|
372
|
+
);
|
|
373
|
+
if (decreased !== this._currentConcurrency) {
|
|
374
|
+
const prev = this._currentConcurrency;
|
|
375
|
+
this._currentConcurrency = decreased;
|
|
376
|
+
this.onConcurrencyChange(
|
|
377
|
+
prev,
|
|
378
|
+
decreased,
|
|
379
|
+
`Average latency (${Math.round(average)}ms) exceeded target (${this.targetLatencyMs}ms). Scaled down.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
} else if (average < this.targetLatencyMs * 0.75) {
|
|
383
|
+
const increased = Math.min(this.maxConcurrency, this._currentConcurrency + 1);
|
|
384
|
+
if (increased !== this._currentConcurrency) {
|
|
385
|
+
const prev = this._currentConcurrency;
|
|
386
|
+
this._currentConcurrency = increased;
|
|
387
|
+
this.onConcurrencyChange(
|
|
388
|
+
prev,
|
|
389
|
+
increased,
|
|
390
|
+
`Average latency (${Math.round(average)}ms) below target threshold. Scaled up.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns a snapshot of adaptive telemetry metrics.
|
|
397
|
+
*/
|
|
398
|
+
getStats() {
|
|
399
|
+
return {
|
|
400
|
+
currentConcurrency: this._currentConcurrency,
|
|
401
|
+
averageLatencyMs: this.lastAverageLatencyMs,
|
|
402
|
+
samplesRecorded: this.recentDurations.length
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
260
407
|
// src/models/circuit-breaker.model.ts
|
|
261
408
|
var ECircuitState = /* @__PURE__ */ ((ECircuitState2) => {
|
|
262
409
|
ECircuitState2["CLOSED"] = "closed";
|
|
@@ -374,21 +521,6 @@ var CircuitBreakerCoordinator = class {
|
|
|
374
521
|
}
|
|
375
522
|
};
|
|
376
523
|
|
|
377
|
-
// src/errors/cancellation.error.ts
|
|
378
|
-
var AhkoCancellationError = class extends AhkoError {
|
|
379
|
-
/**
|
|
380
|
-
* Creates a new AhkoCancellationError.
|
|
381
|
-
*
|
|
382
|
-
* @param message - Reason for cancellation.
|
|
383
|
-
* @param options - Standard Error options including cause.
|
|
384
|
-
*/
|
|
385
|
-
constructor(message = "Task was cancelled", options) {
|
|
386
|
-
super(message, options);
|
|
387
|
-
this.name = "AhkoCancellationError";
|
|
388
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
|
|
392
524
|
// src/scheduler/debounce-coordinator.ts
|
|
393
525
|
var DebounceCoordinator = class {
|
|
394
526
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -785,7 +917,7 @@ var AhkoEventEmitter = class {
|
|
|
785
917
|
// src/scheduler/task-queue.ts
|
|
786
918
|
var TaskQueue = class {
|
|
787
919
|
/** Maximum concurrent active tasks */
|
|
788
|
-
|
|
920
|
+
_concurrency;
|
|
789
921
|
/** Minimum interval in milliseconds between consecutive task starts */
|
|
790
922
|
minIntervalMs;
|
|
791
923
|
/** Timestamp of the most recent task start */
|
|
@@ -802,6 +934,8 @@ var TaskQueue = class {
|
|
|
802
934
|
idleEntries = /* @__PURE__ */ new Set();
|
|
803
935
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
804
936
|
retryEntries = /* @__PURE__ */ new Set();
|
|
937
|
+
/** Tag index for selective cancellation and task classification */
|
|
938
|
+
tagIndex = /* @__PURE__ */ new Map();
|
|
805
939
|
/** Coordinator for debounced tasks with key coalescing */
|
|
806
940
|
debounceCoordinator = new DebounceCoordinator();
|
|
807
941
|
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
@@ -810,6 +944,8 @@ var TaskQueue = class {
|
|
|
810
944
|
emitter = new AhkoEventEmitter();
|
|
811
945
|
/** Circuit breaker coordinator if configured */
|
|
812
946
|
circuitBreakerCoordinator;
|
|
947
|
+
/** Adaptive concurrency coordinator if configured */
|
|
948
|
+
adaptiveCoordinator;
|
|
813
949
|
/** Pause state flag */
|
|
814
950
|
_isPaused = false;
|
|
815
951
|
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
@@ -834,9 +970,10 @@ var TaskQueue = class {
|
|
|
834
970
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
835
971
|
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
836
972
|
* @param circuitBreakerOptions - Optional circuit breaker policy configuration.
|
|
973
|
+
* @param adaptiveOptions - Optional adaptive concurrency policy configuration.
|
|
837
974
|
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
838
975
|
*/
|
|
839
|
-
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions) {
|
|
976
|
+
constructor(concurrency = Infinity, minIntervalMs = 0, circuitBreakerOptions, adaptiveOptions) {
|
|
840
977
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
841
978
|
throw new AhkoConfigurationError(
|
|
842
979
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
@@ -847,14 +984,62 @@ var TaskQueue = class {
|
|
|
847
984
|
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
848
985
|
);
|
|
849
986
|
}
|
|
850
|
-
this.
|
|
987
|
+
this._concurrency = concurrency;
|
|
851
988
|
this.minIntervalMs = minIntervalMs;
|
|
852
989
|
if (circuitBreakerOptions) {
|
|
853
990
|
this.circuitBreakerCoordinator = new CircuitBreakerCoordinator(circuitBreakerOptions);
|
|
854
991
|
}
|
|
992
|
+
if (adaptiveOptions) {
|
|
993
|
+
this.adaptiveCoordinator = new AdaptiveCoordinator(
|
|
994
|
+
adaptiveOptions,
|
|
995
|
+
this._concurrency,
|
|
996
|
+
(previous, current, reason) => {
|
|
997
|
+
this._concurrency = current;
|
|
998
|
+
this.emitter.emit("concurrency:change", {
|
|
999
|
+
previousConcurrency: previous,
|
|
1000
|
+
currentConcurrency: current,
|
|
1001
|
+
reason
|
|
1002
|
+
});
|
|
1003
|
+
this.pump();
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
this._concurrency = this.adaptiveCoordinator.currentConcurrency;
|
|
1007
|
+
}
|
|
855
1008
|
this.debounceCoordinator.onSettled = () => this.checkIdle();
|
|
856
1009
|
this.throttleCoordinator.onSettled = () => this.checkIdle();
|
|
857
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Current concurrency capacity limit.
|
|
1013
|
+
*/
|
|
1014
|
+
get concurrency() {
|
|
1015
|
+
return this._concurrency;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Dynamically adjusts the concurrency limit at runtime.
|
|
1019
|
+
*
|
|
1020
|
+
* @param newConcurrency - New maximum concurrency (must be >= 1).
|
|
1021
|
+
* @throws {AhkoConfigurationError} If newConcurrency is less than 1.
|
|
1022
|
+
*/
|
|
1023
|
+
setConcurrency(newConcurrency) {
|
|
1024
|
+
if (Number.isNaN(newConcurrency) || newConcurrency < 1) {
|
|
1025
|
+
throw new AhkoConfigurationError(
|
|
1026
|
+
`Invalid concurrency "${newConcurrency}". Must be a number greater than or equal to 1.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const previous = this._concurrency;
|
|
1030
|
+
this._concurrency = newConcurrency;
|
|
1031
|
+
if (this.adaptiveCoordinator) {
|
|
1032
|
+
this.adaptiveCoordinator.setConcurrency(newConcurrency);
|
|
1033
|
+
}
|
|
1034
|
+
if (newConcurrency !== previous) {
|
|
1035
|
+
this.emitter.emit("concurrency:change", {
|
|
1036
|
+
previousConcurrency: previous,
|
|
1037
|
+
currentConcurrency: newConcurrency,
|
|
1038
|
+
reason: "Manual concurrency update"
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
this.pump();
|
|
1042
|
+
}
|
|
858
1043
|
/**
|
|
859
1044
|
* Pauses queue execution. Running tasks will complete normally, but no new pending tasks will be dispatched.
|
|
860
1045
|
*/
|
|
@@ -876,6 +1061,76 @@ var TaskQueue = class {
|
|
|
876
1061
|
isPaused() {
|
|
877
1062
|
return this._isPaused;
|
|
878
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Cancels all pending, delayed, and active tasks marked with the specified tag.
|
|
1066
|
+
*
|
|
1067
|
+
* @param tag - Tag identifier to match.
|
|
1068
|
+
* @param reason - Optional cancellation reason.
|
|
1069
|
+
* @returns Total count of tasks cancelled.
|
|
1070
|
+
*/
|
|
1071
|
+
cancelByTag(tag, reason) {
|
|
1072
|
+
const runners = this.tagIndex.get(tag);
|
|
1073
|
+
if (!runners || runners.size === 0) {
|
|
1074
|
+
return 0;
|
|
1075
|
+
}
|
|
1076
|
+
const list = Array.from(runners);
|
|
1077
|
+
let count = 0;
|
|
1078
|
+
for (const runner of list) {
|
|
1079
|
+
if (runner.state === "pending" /* PENDING */ || runner.state === "running" /* RUNNING */) {
|
|
1080
|
+
runner.cancel(reason ?? `Task cancelled by tag "${tag}"`);
|
|
1081
|
+
count++;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return count;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Returns active and pending task counts for a given tag.
|
|
1088
|
+
*
|
|
1089
|
+
* @param tag - Tag identifier.
|
|
1090
|
+
*/
|
|
1091
|
+
getStatsByTag(tag) {
|
|
1092
|
+
const runners = this.tagIndex.get(tag);
|
|
1093
|
+
if (!runners) {
|
|
1094
|
+
return { activeTasks: 0, pendingTasks: 0 };
|
|
1095
|
+
}
|
|
1096
|
+
let active = 0;
|
|
1097
|
+
let pending = 0;
|
|
1098
|
+
for (const runner of runners) {
|
|
1099
|
+
if (runner.state === "running" /* RUNNING */) {
|
|
1100
|
+
active++;
|
|
1101
|
+
} else if (runner.state === "pending" /* PENDING */) {
|
|
1102
|
+
pending++;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return { activeTasks: active, pendingTasks: pending };
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Indexes a runner under all its associated tags.
|
|
1109
|
+
*/
|
|
1110
|
+
indexTaskTags(runner) {
|
|
1111
|
+
for (const tag of runner.tags) {
|
|
1112
|
+
let set = this.tagIndex.get(tag);
|
|
1113
|
+
if (!set) {
|
|
1114
|
+
set = /* @__PURE__ */ new Set();
|
|
1115
|
+
this.tagIndex.set(tag, set);
|
|
1116
|
+
}
|
|
1117
|
+
set.add(runner);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Removes a runner from the tag index upon settlement.
|
|
1122
|
+
*/
|
|
1123
|
+
cleanupTaskTags(runner) {
|
|
1124
|
+
for (const tag of runner.tags) {
|
|
1125
|
+
const set = this.tagIndex.get(tag);
|
|
1126
|
+
if (set) {
|
|
1127
|
+
set.delete(runner);
|
|
1128
|
+
if (set.size === 0) {
|
|
1129
|
+
this.tagIndex.delete(tag);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
879
1134
|
/**
|
|
880
1135
|
* Inserts a task runner into the queue based on priority weight (descending).
|
|
881
1136
|
* Preserves FIFO ordering among tasks with identical priority.
|
|
@@ -957,6 +1212,11 @@ var TaskQueue = class {
|
|
|
957
1212
|
if (options) {
|
|
958
1213
|
this.runnerOptions.set(runner, options);
|
|
959
1214
|
}
|
|
1215
|
+
this.indexTaskTags(runner);
|
|
1216
|
+
runner.promise.finally(() => {
|
|
1217
|
+
this.cleanupTaskTags(runner);
|
|
1218
|
+
}).catch(() => {
|
|
1219
|
+
});
|
|
960
1220
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
961
1221
|
this.cancelledTasks++;
|
|
962
1222
|
return runner.promise;
|
|
@@ -1135,7 +1395,7 @@ var TaskQueue = class {
|
|
|
1135
1395
|
* and queue is not paused.
|
|
1136
1396
|
*/
|
|
1137
1397
|
pump() {
|
|
1138
|
-
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this.
|
|
1398
|
+
if (this._isPaused || this.queue.length === 0 || this.activeRunners.size >= this._concurrency) {
|
|
1139
1399
|
return;
|
|
1140
1400
|
}
|
|
1141
1401
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
@@ -1152,7 +1412,7 @@ var TaskQueue = class {
|
|
|
1152
1412
|
return;
|
|
1153
1413
|
}
|
|
1154
1414
|
}
|
|
1155
|
-
while (!this._isPaused && this.activeRunners.size < this.
|
|
1415
|
+
while (!this._isPaused && this.activeRunners.size < this._concurrency && this.queue.length > 0) {
|
|
1156
1416
|
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
1157
1417
|
const now = Date.now();
|
|
1158
1418
|
const elapsed = now - this.lastTaskStartTime;
|
|
@@ -1194,7 +1454,7 @@ var TaskQueue = class {
|
|
|
1194
1454
|
this.lastTaskStartTime = Date.now();
|
|
1195
1455
|
void this.executeRunner(runner);
|
|
1196
1456
|
if (this.minIntervalMs > 0) {
|
|
1197
|
-
if (this.queue.length > 0 && this.activeRunners.size < this.
|
|
1457
|
+
if (this.queue.length > 0 && this.activeRunners.size < this._concurrency) {
|
|
1198
1458
|
if (this.rateLimitTimer === void 0) {
|
|
1199
1459
|
this.rateLimitTimer = setTimeout(() => {
|
|
1200
1460
|
this.rateLimitTimer = void 0;
|
|
@@ -1219,6 +1479,7 @@ var TaskQueue = class {
|
|
|
1219
1479
|
try {
|
|
1220
1480
|
const result = await runner.run();
|
|
1221
1481
|
this.circuitBreakerCoordinator?.recordSuccess();
|
|
1482
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
1222
1483
|
this.completedTasks++;
|
|
1223
1484
|
this.activeRunners.delete(runner);
|
|
1224
1485
|
this.runnerOptions.delete(runner);
|
|
@@ -1257,6 +1518,7 @@ var TaskQueue = class {
|
|
|
1257
1518
|
if (this.circuitBreakerCoordinator && !(error instanceof AhkoCircuitBreakerOpenError)) {
|
|
1258
1519
|
this.circuitBreakerCoordinator.recordFailure(error);
|
|
1259
1520
|
}
|
|
1521
|
+
this.adaptiveCoordinator?.recordDuration(runner.lastDurationMs);
|
|
1260
1522
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
1261
1523
|
this.timedOutTasks++;
|
|
1262
1524
|
this.emitter.emit("task:timeout", {
|
|
@@ -1456,9 +1718,10 @@ var TaskQueue = class {
|
|
|
1456
1718
|
timedOutTasks: this.timedOutTasks,
|
|
1457
1719
|
retriedTasks: this.retriedTasks,
|
|
1458
1720
|
totalDispatched: this.totalDispatched,
|
|
1459
|
-
capacity: this.
|
|
1721
|
+
capacity: this._concurrency,
|
|
1460
1722
|
isPaused: this._isPaused,
|
|
1461
|
-
circuitState: this.circuitBreakerCoordinator?.state
|
|
1723
|
+
circuitState: this.circuitBreakerCoordinator?.state,
|
|
1724
|
+
adaptive: this.adaptiveCoordinator?.getStats()
|
|
1462
1725
|
});
|
|
1463
1726
|
}
|
|
1464
1727
|
};
|
|
@@ -1494,18 +1757,22 @@ var TaskRunner = class {
|
|
|
1494
1757
|
attempt = 1;
|
|
1495
1758
|
/** Duration of the most recent execution attempt in milliseconds */
|
|
1496
1759
|
lastDurationMs = 0;
|
|
1760
|
+
/** Set of classification tags associated with this task */
|
|
1761
|
+
tags;
|
|
1497
1762
|
/**
|
|
1498
1763
|
* Creates a new TaskRunner instance.
|
|
1499
1764
|
*
|
|
1500
1765
|
* @param task - The asynchronous work unit to run.
|
|
1501
1766
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
1502
1767
|
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
1768
|
+
* @param tags - Optional array of tags for classifying and selectively cancelling tasks.
|
|
1503
1769
|
*/
|
|
1504
|
-
constructor(task, externalSignal, timeoutMs) {
|
|
1770
|
+
constructor(task, externalSignal, timeoutMs, tags) {
|
|
1505
1771
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1506
1772
|
this.task = task;
|
|
1507
1773
|
this.externalSignal = externalSignal;
|
|
1508
1774
|
this.timeoutMs = timeoutMs;
|
|
1775
|
+
this.tags = new Set(tags ?? []);
|
|
1509
1776
|
this.abortController = new AbortController();
|
|
1510
1777
|
this.promise = new Promise((resolve, reject) => {
|
|
1511
1778
|
this.resolvePromise = resolve;
|
|
@@ -1832,7 +2099,8 @@ var Ahko = class _Ahko {
|
|
|
1832
2099
|
return new _Ahko({
|
|
1833
2100
|
...profile,
|
|
1834
2101
|
...overrides,
|
|
1835
|
-
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker
|
|
2102
|
+
circuitBreaker: overrides?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2103
|
+
adaptive: overrides?.adaptive ?? profile?.adaptive
|
|
1836
2104
|
});
|
|
1837
2105
|
}
|
|
1838
2106
|
/**
|
|
@@ -1851,22 +2119,40 @@ var Ahko = class _Ahko {
|
|
|
1851
2119
|
const mergedOptions = {
|
|
1852
2120
|
...profile,
|
|
1853
2121
|
...options,
|
|
1854
|
-
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker
|
|
2122
|
+
circuitBreaker: options?.circuitBreaker ?? profile?.circuitBreaker,
|
|
2123
|
+
adaptive: options?.adaptive ?? profile?.adaptive
|
|
1855
2124
|
};
|
|
1856
2125
|
if (profile) {
|
|
1857
2126
|
this.defaultScheduleOptions = {
|
|
1858
2127
|
priority: profile.priority,
|
|
1859
2128
|
retry: profile.retry,
|
|
1860
2129
|
timeoutMs: profile.timeoutMs,
|
|
1861
|
-
totalTimeoutMs: profile.totalTimeoutMs
|
|
2130
|
+
totalTimeoutMs: profile.totalTimeoutMs,
|
|
2131
|
+
tags: profile.tags
|
|
1862
2132
|
};
|
|
1863
2133
|
}
|
|
1864
2134
|
this.queue = new TaskQueue(
|
|
1865
2135
|
mergedOptions.concurrency,
|
|
1866
2136
|
mergedOptions.minIntervalMs,
|
|
1867
|
-
mergedOptions.circuitBreaker
|
|
2137
|
+
mergedOptions.circuitBreaker,
|
|
2138
|
+
mergedOptions.adaptive
|
|
1868
2139
|
);
|
|
1869
2140
|
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Current concurrency limit.
|
|
2143
|
+
*/
|
|
2144
|
+
get concurrency() {
|
|
2145
|
+
return this.queue.concurrency;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Dynamically updates the concurrency limit of the scheduler.
|
|
2149
|
+
*
|
|
2150
|
+
* @param concurrency - New maximum concurrency (must be >= 1).
|
|
2151
|
+
* @throws {AhkoConfigurationError} If concurrency is invalid.
|
|
2152
|
+
*/
|
|
2153
|
+
setConcurrency(concurrency) {
|
|
2154
|
+
this.queue.setConcurrency(concurrency);
|
|
2155
|
+
}
|
|
1870
2156
|
/**
|
|
1871
2157
|
* Pauses scheduler dispatch. In-flight tasks run to completion, but pending tasks remain queued.
|
|
1872
2158
|
*/
|
|
@@ -1946,9 +2232,11 @@ var Ahko = class _Ahko {
|
|
|
1946
2232
|
if (typeof task !== "function") {
|
|
1947
2233
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
1948
2234
|
}
|
|
2235
|
+
const mergedTags = options?.tags ?? this.defaultScheduleOptions?.tags;
|
|
1949
2236
|
const mergedOptions = {
|
|
1950
2237
|
...this.defaultScheduleOptions,
|
|
1951
|
-
...options
|
|
2238
|
+
...options,
|
|
2239
|
+
tags: mergedTags
|
|
1952
2240
|
};
|
|
1953
2241
|
const strategy = mergedOptions.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1954
2242
|
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
@@ -1991,7 +2279,12 @@ var Ahko = class _Ahko {
|
|
|
1991
2279
|
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1992
2280
|
);
|
|
1993
2281
|
}
|
|
1994
|
-
const runner = new TaskRunner(
|
|
2282
|
+
const runner = new TaskRunner(
|
|
2283
|
+
task,
|
|
2284
|
+
mergedOptions.signal,
|
|
2285
|
+
mergedOptions.timeoutMs,
|
|
2286
|
+
mergedOptions.tags
|
|
2287
|
+
);
|
|
1995
2288
|
return this.queue.enqueue(runner, mergedOptions);
|
|
1996
2289
|
}
|
|
1997
2290
|
/**
|
|
@@ -2052,6 +2345,176 @@ var Ahko = class _Ahko {
|
|
|
2052
2345
|
waitMs
|
|
2053
2346
|
});
|
|
2054
2347
|
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Transforms an iterable of items concurrently using an asynchronous mapping function.
|
|
2350
|
+
*
|
|
2351
|
+
* Results are guaranteed to be returned in the original index order.
|
|
2352
|
+
* Concurrency can be capped per-batch or fall back to the scheduler's global limit.
|
|
2353
|
+
*
|
|
2354
|
+
* @template TItem - Type of input elements.
|
|
2355
|
+
* @template TResult - Type of mapped elements.
|
|
2356
|
+
* @param items - Iterable sequence of items to process.
|
|
2357
|
+
* @param fn - Mapper callback receiving item, index, and task context.
|
|
2358
|
+
* @param options - Batch execution options (concurrency, stopOnError, retry, signal, tags, etc.).
|
|
2359
|
+
* @returns Array of transformed results in index order.
|
|
2360
|
+
*
|
|
2361
|
+
* @throws {AhkoConfigurationError} If fn is not a function or concurrency is invalid.
|
|
2362
|
+
* @throws {AhkoCancellationError} If batch or item is cancelled.
|
|
2363
|
+
*
|
|
2364
|
+
* @example
|
|
2365
|
+
* ```typescript
|
|
2366
|
+
* const urls = ["/api/1", "/api/2", "/api/3"];
|
|
2367
|
+
* const data = await ahko.map(urls, async (url, i, { signal }) => {
|
|
2368
|
+
* const res = await fetch(url, { signal });
|
|
2369
|
+
* return res.json();
|
|
2370
|
+
* }, { concurrency: 2 });
|
|
2371
|
+
* ```
|
|
2372
|
+
*/
|
|
2373
|
+
async map(items, fn, options) {
|
|
2374
|
+
if (typeof fn !== "function") {
|
|
2375
|
+
throw new AhkoConfigurationError("Mapper function must be a valid function.");
|
|
2376
|
+
}
|
|
2377
|
+
if (options?.concurrency !== void 0 && (typeof options.concurrency !== "number" || Number.isNaN(options.concurrency) || options.concurrency < 1)) {
|
|
2378
|
+
throw new AhkoConfigurationError(
|
|
2379
|
+
`Invalid concurrency "${options.concurrency}". Must be a number greater than or equal to 1.`
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
const list = Array.from(items);
|
|
2383
|
+
if (list.length === 0) {
|
|
2384
|
+
return [];
|
|
2385
|
+
}
|
|
2386
|
+
const { concurrency, stopOnError = false, signal: externalSignal, ...scheduleOpts } = options ?? {};
|
|
2387
|
+
if (externalSignal?.aborted) {
|
|
2388
|
+
throw new AhkoCancellationError(
|
|
2389
|
+
externalSignal.reason ? `Batch cancelled: ${String(externalSignal.reason)}` : "Batch cancelled"
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
const abortController = new AbortController();
|
|
2393
|
+
const results = new Array(list.length);
|
|
2394
|
+
let firstError = void 0;
|
|
2395
|
+
let hasAborted = false;
|
|
2396
|
+
const localizedLimit = concurrency !== void 0 ? Math.floor(concurrency) : Number.isFinite(this.concurrency) ? this.concurrency : Infinity;
|
|
2397
|
+
return new Promise((resolve, reject) => {
|
|
2398
|
+
let currentIndex = 0;
|
|
2399
|
+
let activeCount = 0;
|
|
2400
|
+
let settledCount = 0;
|
|
2401
|
+
const onExternalAbort = () => {
|
|
2402
|
+
const reason = externalSignal?.reason ?? "Batch cancelled by external signal";
|
|
2403
|
+
const err = new AhkoCancellationError(
|
|
2404
|
+
typeof reason === "string" ? reason : "Batch cancelled by external signal"
|
|
2405
|
+
);
|
|
2406
|
+
cleanupAndReject(err);
|
|
2407
|
+
};
|
|
2408
|
+
if (externalSignal) {
|
|
2409
|
+
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
2410
|
+
}
|
|
2411
|
+
const cleanupAndReject = (err) => {
|
|
2412
|
+
if (!hasAborted) {
|
|
2413
|
+
hasAborted = true;
|
|
2414
|
+
abortController.abort(err);
|
|
2415
|
+
}
|
|
2416
|
+
if (externalSignal) {
|
|
2417
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2418
|
+
}
|
|
2419
|
+
reject(err);
|
|
2420
|
+
};
|
|
2421
|
+
const checkCompletion = () => {
|
|
2422
|
+
if (settledCount === list.length) {
|
|
2423
|
+
if (externalSignal) {
|
|
2424
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
2425
|
+
}
|
|
2426
|
+
if (firstError !== void 0) {
|
|
2427
|
+
reject(firstError);
|
|
2428
|
+
} else {
|
|
2429
|
+
resolve(results);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
const launchNext = () => {
|
|
2434
|
+
if (hasAborted && stopOnError) {
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
while (currentIndex < list.length && activeCount < localizedLimit && !(hasAborted && stopOnError)) {
|
|
2438
|
+
const index = currentIndex++;
|
|
2439
|
+
const item = list[index];
|
|
2440
|
+
activeCount++;
|
|
2441
|
+
const taskPromise = this.schedule(
|
|
2442
|
+
(context) => fn(item, index, context),
|
|
2443
|
+
{
|
|
2444
|
+
...scheduleOpts,
|
|
2445
|
+
signal: abortController.signal
|
|
2446
|
+
}
|
|
2447
|
+
);
|
|
2448
|
+
taskPromise.then((result) => {
|
|
2449
|
+
results[index] = result;
|
|
2450
|
+
}).catch((err) => {
|
|
2451
|
+
if (firstError === void 0) {
|
|
2452
|
+
firstError = err;
|
|
2453
|
+
}
|
|
2454
|
+
if (stopOnError && !hasAborted) {
|
|
2455
|
+
cleanupAndReject(err);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
}).finally(() => {
|
|
2459
|
+
activeCount--;
|
|
2460
|
+
settledCount++;
|
|
2461
|
+
if (hasAborted && stopOnError) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (currentIndex < list.length) {
|
|
2465
|
+
launchNext();
|
|
2466
|
+
} else {
|
|
2467
|
+
checkCompletion();
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
if (abortController.signal.aborted) {
|
|
2473
|
+
cleanupAndReject(abortController.signal.reason);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
launchNext();
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Iterates sequentially or concurrently over an iterable sequence of items,
|
|
2481
|
+
* executing the callback function for each element.
|
|
2482
|
+
*
|
|
2483
|
+
* @template TItem - Type of input elements.
|
|
2484
|
+
* @param items - Iterable sequence of items to process.
|
|
2485
|
+
* @param fn - Callback receiving item, index, and task context.
|
|
2486
|
+
* @param options - Batch execution options.
|
|
2487
|
+
* @returns Promise resolving once all items have finished executing.
|
|
2488
|
+
*
|
|
2489
|
+
* @example
|
|
2490
|
+
* ```typescript
|
|
2491
|
+
* await ahko.each(userQueue, async (user, index, { signal }) => {
|
|
2492
|
+
* await sendWelcomeEmail(user, { signal });
|
|
2493
|
+
* }, { concurrency: 5 });
|
|
2494
|
+
* ```
|
|
2495
|
+
*/
|
|
2496
|
+
async each(items, fn, options) {
|
|
2497
|
+
await this.map(items, fn, options);
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Cancels all pending, delayed, and active tasks tagged with the given tag.
|
|
2501
|
+
*
|
|
2502
|
+
* @param tag - Tag identifier.
|
|
2503
|
+
* @param reason - Optional cancellation reason.
|
|
2504
|
+
* @returns Total number of tasks cancelled.
|
|
2505
|
+
*/
|
|
2506
|
+
cancelByTag(tag, reason) {
|
|
2507
|
+
return this.queue.cancelByTag(tag, reason);
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Retrieves active and pending task counts for a given tag.
|
|
2511
|
+
*
|
|
2512
|
+
* @param tag - Tag identifier.
|
|
2513
|
+
* @returns Object with activeTasks and pendingTasks counts.
|
|
2514
|
+
*/
|
|
2515
|
+
statsByTag(tag) {
|
|
2516
|
+
return this.queue.getStatsByTag(tag);
|
|
2517
|
+
}
|
|
2055
2518
|
/**
|
|
2056
2519
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
2057
2520
|
*
|
|
@@ -2146,7 +2609,7 @@ var Ahko = class _Ahko {
|
|
|
2146
2609
|
};
|
|
2147
2610
|
|
|
2148
2611
|
// src/version.ts
|
|
2149
|
-
var VERSION = "1.1.
|
|
2612
|
+
var VERSION = "1.1.6";
|
|
2150
2613
|
|
|
2151
2614
|
// src/errors/queue.error.ts
|
|
2152
2615
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -2219,6 +2682,7 @@ function combineSignals(signals) {
|
|
|
2219
2682
|
}
|
|
2220
2683
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2221
2684
|
0 && (module.exports = {
|
|
2685
|
+
AdaptiveCoordinator,
|
|
2222
2686
|
Ahko,
|
|
2223
2687
|
AhkoCancellationError,
|
|
2224
2688
|
AhkoCircuitBreakerOpenError,
|