@mrjacket/ahko 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +94 -8
- package/dist/errors/timeout.error.d.ts +15 -2
- package/dist/index.cjs +346 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +341 -25
- package/dist/index.js.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/options.model.d.ts +12 -0
- package/dist/models/retry.model.d.ts +52 -0
- package/dist/retry/backoff.d.ts +18 -0
- package/dist/retry/index.d.ts +1 -0
- package/dist/scheduler/signal.d.ts +20 -0
- package/dist/scheduler/task-queue.d.ts +9 -1
- package/dist/scheduler/task-runner.d.ts +25 -5
- package/dist/version.d.ts +1 -1
- package/package.json +21 -2
package/dist/index.cjs
CHANGED
|
@@ -26,9 +26,13 @@ __export(src_exports, {
|
|
|
26
26
|
AhkoError: () => AhkoError,
|
|
27
27
|
AhkoQueueError: () => AhkoQueueError,
|
|
28
28
|
AhkoTimeoutError: () => AhkoTimeoutError,
|
|
29
|
+
DEFAULT_BASE_DELAY: () => DEFAULT_BASE_DELAY,
|
|
30
|
+
DEFAULT_MAX_DELAY: () => DEFAULT_MAX_DELAY,
|
|
29
31
|
EScheduleStrategy: () => EScheduleStrategy,
|
|
30
32
|
ETaskState: () => ETaskState,
|
|
31
|
-
VERSION: () => VERSION
|
|
33
|
+
VERSION: () => VERSION,
|
|
34
|
+
calculateBackoff: () => calculateBackoff,
|
|
35
|
+
combineSignals: () => combineSignals
|
|
32
36
|
});
|
|
33
37
|
module.exports = __toCommonJS(src_exports);
|
|
34
38
|
|
|
@@ -70,6 +74,26 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
70
74
|
return EScheduleStrategy2;
|
|
71
75
|
})(EScheduleStrategy || {});
|
|
72
76
|
|
|
77
|
+
// src/errors/timeout.error.ts
|
|
78
|
+
var AhkoTimeoutError = class extends AhkoError {
|
|
79
|
+
/**
|
|
80
|
+
* The timeout threshold in milliseconds that was exceeded, if configured.
|
|
81
|
+
*/
|
|
82
|
+
timeoutMs;
|
|
83
|
+
/**
|
|
84
|
+
* Creates a new AhkoTimeoutError.
|
|
85
|
+
*
|
|
86
|
+
* @param message - Explanation of timeout expiry.
|
|
87
|
+
* @param options - Standard Error options including optional timeoutMs and cause.
|
|
88
|
+
*/
|
|
89
|
+
constructor(message = "Task execution timed out", options) {
|
|
90
|
+
super(message, options);
|
|
91
|
+
this.name = "AhkoTimeoutError";
|
|
92
|
+
this.timeoutMs = options?.timeoutMs;
|
|
93
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
73
97
|
// src/models/state.model.ts
|
|
74
98
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
75
99
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -81,6 +105,31 @@ var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
|
81
105
|
return ETaskState2;
|
|
82
106
|
})(ETaskState || {});
|
|
83
107
|
|
|
108
|
+
// src/retry/backoff.ts
|
|
109
|
+
var DEFAULT_BASE_DELAY = 250;
|
|
110
|
+
var DEFAULT_MAX_DELAY = 1e4;
|
|
111
|
+
function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
112
|
+
const backoff = options?.backoff ?? "exponential";
|
|
113
|
+
if (backoff === "none") {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const baseDelay = typeof options?.baseDelay === "number" && !Number.isNaN(options.baseDelay) && options.baseDelay >= 0 ? options.baseDelay : DEFAULT_BASE_DELAY;
|
|
117
|
+
const maxDelay = typeof options?.maxDelay === "number" && !Number.isNaN(options.maxDelay) && options.maxDelay >= baseDelay ? options.maxDelay : Math.max(DEFAULT_MAX_DELAY, baseDelay);
|
|
118
|
+
let calculatedDelay;
|
|
119
|
+
if (backoff === "linear") {
|
|
120
|
+
calculatedDelay = baseDelay * Math.max(1, attempt);
|
|
121
|
+
} else {
|
|
122
|
+
const exponent = Math.max(0, attempt - 1);
|
|
123
|
+
const factor = exponent > 30 ? 2 ** 30 : 2 ** exponent;
|
|
124
|
+
calculatedDelay = baseDelay * factor;
|
|
125
|
+
}
|
|
126
|
+
const cappedDelay = Math.min(calculatedDelay, maxDelay);
|
|
127
|
+
if (options?.jitter) {
|
|
128
|
+
return Math.floor(randomFn() * (cappedDelay + 1));
|
|
129
|
+
}
|
|
130
|
+
return Math.floor(cappedDelay);
|
|
131
|
+
}
|
|
132
|
+
|
|
84
133
|
// src/scheduler/idle-scheduler.ts
|
|
85
134
|
var IdleScheduler = class {
|
|
86
135
|
/**
|
|
@@ -132,6 +181,10 @@ var TaskQueue = class {
|
|
|
132
181
|
delayedEntries = /* @__PURE__ */ new Set();
|
|
133
182
|
/** Set of tasks currently awaiting an idle opportunity */
|
|
134
183
|
idleEntries = /* @__PURE__ */ new Set();
|
|
184
|
+
/** Set of tasks currently awaiting a retry backoff timer */
|
|
185
|
+
retryEntries = /* @__PURE__ */ new Set();
|
|
186
|
+
/** WeakMap associating task runners with their scheduling options */
|
|
187
|
+
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
135
188
|
/** Cumulative completed tasks counter */
|
|
136
189
|
completedTasks = 0;
|
|
137
190
|
/** Cumulative failed tasks counter */
|
|
@@ -170,6 +223,33 @@ var TaskQueue = class {
|
|
|
170
223
|
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
171
224
|
);
|
|
172
225
|
}
|
|
226
|
+
if (options?.retry) {
|
|
227
|
+
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
228
|
+
throw new AhkoConfigurationError(
|
|
229
|
+
`Invalid retry attempts "${options.retry.attempts}". attempts must be an integer greater than or equal to 1.`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (options.retry.baseDelay !== void 0 && (typeof options.retry.baseDelay !== "number" || Number.isNaN(options.retry.baseDelay) || options.retry.baseDelay < 0)) {
|
|
233
|
+
throw new AhkoConfigurationError(
|
|
234
|
+
`Invalid retry baseDelay "${options.retry.baseDelay}". baseDelay must be a non-negative number in milliseconds.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (options.retry.maxDelay !== void 0 && (typeof options.retry.maxDelay !== "number" || Number.isNaN(options.retry.maxDelay) || options.retry.maxDelay < 0)) {
|
|
238
|
+
throw new AhkoConfigurationError(
|
|
239
|
+
`Invalid retry maxDelay "${options.retry.maxDelay}". maxDelay must be a non-negative number in milliseconds.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (options?.timeoutMs !== void 0) {
|
|
244
|
+
if (typeof options.timeoutMs !== "number" || Number.isNaN(options.timeoutMs) || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
245
|
+
throw new AhkoConfigurationError(
|
|
246
|
+
`Invalid timeoutMs "${options.timeoutMs}". timeoutMs must be a positive finite number greater than 0.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (options) {
|
|
251
|
+
this.runnerOptions.set(runner, options);
|
|
252
|
+
}
|
|
173
253
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
174
254
|
this.cancelledTasks++;
|
|
175
255
|
return runner.promise;
|
|
@@ -286,28 +366,86 @@ var TaskQueue = class {
|
|
|
286
366
|
}
|
|
287
367
|
/**
|
|
288
368
|
* Internal execution of an active task runner.
|
|
289
|
-
* Settle caller promise strictly after stats and active status are updated.
|
|
290
369
|
*/
|
|
291
370
|
async executeRunner(runner) {
|
|
371
|
+
const options = this.runnerOptions.get(runner);
|
|
292
372
|
try {
|
|
293
373
|
const result = await runner.run();
|
|
294
374
|
this.completedTasks++;
|
|
295
375
|
this.activeRunners.delete(runner);
|
|
376
|
+
this.runnerOptions.delete(runner);
|
|
296
377
|
runner.resolve(result);
|
|
297
378
|
} catch (error) {
|
|
298
379
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
299
380
|
this.cancelledTasks++;
|
|
300
|
-
|
|
381
|
+
this.activeRunners.delete(runner);
|
|
382
|
+
this.runnerOptions.delete(runner);
|
|
383
|
+
runner.reject(error);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const shouldRetry = await runner.canRetry(error, options?.retry);
|
|
387
|
+
if (shouldRetry) {
|
|
388
|
+
this.activeRunners.delete(runner);
|
|
389
|
+
this.scheduleRetry(runner, options);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
301
393
|
this.timedOutTasks++;
|
|
302
394
|
} else {
|
|
303
395
|
this.failedTasks++;
|
|
304
396
|
}
|
|
305
397
|
this.activeRunners.delete(runner);
|
|
398
|
+
this.runnerOptions.delete(runner);
|
|
306
399
|
runner.reject(error);
|
|
307
400
|
} finally {
|
|
308
401
|
this.pump();
|
|
309
402
|
}
|
|
310
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Schedules a retry attempt following backoff delay,
|
|
406
|
+
* without holding a concurrency slot.
|
|
407
|
+
*/
|
|
408
|
+
scheduleRetry(runner, options) {
|
|
409
|
+
const backoffDelay = calculateBackoff(runner.attempt - 1, options?.retry);
|
|
410
|
+
if (backoffDelay === 0) {
|
|
411
|
+
runner.onCancel = () => {
|
|
412
|
+
const index = this.queue.indexOf(runner);
|
|
413
|
+
if (index !== -1) {
|
|
414
|
+
this.queue.splice(index, 1);
|
|
415
|
+
this.cancelledTasks++;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
this.queue.push(runner);
|
|
419
|
+
this.pump();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const retryEntry = {
|
|
423
|
+
runner,
|
|
424
|
+
timerId: setTimeout(() => {
|
|
425
|
+
this.retryEntries.delete(retryEntry);
|
|
426
|
+
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
runner.onCancel = () => {
|
|
430
|
+
const index = this.queue.indexOf(runner);
|
|
431
|
+
if (index !== -1) {
|
|
432
|
+
this.queue.splice(index, 1);
|
|
433
|
+
this.cancelledTasks++;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
this.queue.push(runner);
|
|
437
|
+
this.pump();
|
|
438
|
+
}, backoffDelay)
|
|
439
|
+
};
|
|
440
|
+
this.retryEntries.add(retryEntry);
|
|
441
|
+
runner.onCancel = () => {
|
|
442
|
+
if (this.retryEntries.has(retryEntry)) {
|
|
443
|
+
clearTimeout(retryEntry.timerId);
|
|
444
|
+
this.retryEntries.delete(retryEntry);
|
|
445
|
+
this.cancelledTasks++;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
311
449
|
/**
|
|
312
450
|
* Returns telemetry snapshot for the scheduler.
|
|
313
451
|
*
|
|
@@ -316,7 +454,7 @@ var TaskQueue = class {
|
|
|
316
454
|
getStats() {
|
|
317
455
|
return Object.freeze({
|
|
318
456
|
activeTasks: this.activeRunners.size,
|
|
319
|
-
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size,
|
|
457
|
+
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size,
|
|
320
458
|
completedTasks: this.completedTasks,
|
|
321
459
|
failedTasks: this.failedTasks,
|
|
322
460
|
cancelledTasks: this.cancelledTasks,
|
|
@@ -354,6 +492,10 @@ var TaskRunner = class {
|
|
|
354
492
|
task;
|
|
355
493
|
/** User-supplied AbortSignal for external cancellation */
|
|
356
494
|
externalSignal;
|
|
495
|
+
/** Maximum execution duration allowed in milliseconds */
|
|
496
|
+
timeoutMs;
|
|
497
|
+
/** Active timeout timer identifier */
|
|
498
|
+
timeoutTimerId;
|
|
357
499
|
/** Abort event listener reference for clean detachment */
|
|
358
500
|
abortListener;
|
|
359
501
|
/** Promise resolve handler */
|
|
@@ -364,16 +506,20 @@ var TaskRunner = class {
|
|
|
364
506
|
promise;
|
|
365
507
|
/** Callback invoked when runner is cancelled while pending */
|
|
366
508
|
onCancel;
|
|
509
|
+
/** Current execution attempt count (1-indexed) */
|
|
510
|
+
attempt = 1;
|
|
367
511
|
/**
|
|
368
512
|
* Creates a new TaskRunner instance.
|
|
369
513
|
*
|
|
370
514
|
* @param task - The asynchronous work unit to run.
|
|
371
515
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
516
|
+
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
372
517
|
*/
|
|
373
|
-
constructor(task, externalSignal) {
|
|
518
|
+
constructor(task, externalSignal, timeoutMs) {
|
|
374
519
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
375
520
|
this.task = task;
|
|
376
521
|
this.externalSignal = externalSignal;
|
|
522
|
+
this.timeoutMs = timeoutMs;
|
|
377
523
|
this.abortController = new AbortController();
|
|
378
524
|
this.promise = new Promise((resolve, reject) => {
|
|
379
525
|
this.resolvePromise = resolve;
|
|
@@ -387,6 +533,7 @@ var TaskRunner = class {
|
|
|
387
533
|
typeof reason === "string" ? reason : "Task was cancelled prior to execution",
|
|
388
534
|
{ cause: reason instanceof Error ? reason : void 0 }
|
|
389
535
|
);
|
|
536
|
+
this.abortController.abort(cancelError);
|
|
390
537
|
this.rejectPromise(cancelError);
|
|
391
538
|
} else {
|
|
392
539
|
this.abortListener = () => {
|
|
@@ -408,6 +555,7 @@ var TaskRunner = class {
|
|
|
408
555
|
* @param value - Value to resolve with.
|
|
409
556
|
*/
|
|
410
557
|
resolve(value) {
|
|
558
|
+
this.cleanup();
|
|
411
559
|
this.resolvePromise(value);
|
|
412
560
|
}
|
|
413
561
|
/**
|
|
@@ -416,12 +564,45 @@ var TaskRunner = class {
|
|
|
416
564
|
* @param reason - Reason to reject with.
|
|
417
565
|
*/
|
|
418
566
|
reject(reason) {
|
|
567
|
+
this.cleanup();
|
|
419
568
|
this.rejectPromise(reason);
|
|
420
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Evaluates if the task should be retried following an execution failure or timeout.
|
|
572
|
+
*
|
|
573
|
+
* @param error - The error encountered during the attempt.
|
|
574
|
+
* @param retryOptions - Configured retry policy.
|
|
575
|
+
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
576
|
+
*/
|
|
577
|
+
async canRetry(error, retryOptions) {
|
|
578
|
+
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (this.attempt >= retryOptions.attempts) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
if (typeof retryOptions.shouldRetry === "function") {
|
|
588
|
+
try {
|
|
589
|
+
const allowed = await retryOptions.shouldRetry(error, this.attempt);
|
|
590
|
+
if (!allowed) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
this.attempt++;
|
|
598
|
+
this._state = "pending" /* PENDING */;
|
|
599
|
+
this.abortController = new AbortController();
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
421
602
|
/**
|
|
422
603
|
* Executes the task within an allocated concurrency slot.
|
|
423
604
|
*
|
|
424
|
-
* @returns A promise resolving to the task result or rejecting on failure/cancellation.
|
|
605
|
+
* @returns A promise resolving to the task result or rejecting on failure/cancellation/timeout.
|
|
425
606
|
*/
|
|
426
607
|
async run() {
|
|
427
608
|
if (this._state === "cancelled" /* CANCELLED */) {
|
|
@@ -432,16 +613,100 @@ var TaskRunner = class {
|
|
|
432
613
|
signal: this.abortController.signal,
|
|
433
614
|
taskId: this.taskId
|
|
434
615
|
};
|
|
616
|
+
let abortListener;
|
|
617
|
+
const abortPromise = new Promise((_, reject) => {
|
|
618
|
+
abortListener = () => {
|
|
619
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
620
|
+
reject(
|
|
621
|
+
new AhkoTimeoutError(
|
|
622
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
623
|
+
{ timeoutMs: this.timeoutMs }
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
} else {
|
|
627
|
+
const reason = this.abortController.signal.reason;
|
|
628
|
+
reject(
|
|
629
|
+
new AhkoCancellationError("Task was cancelled during execution", {
|
|
630
|
+
cause: reason instanceof Error ? reason : void 0
|
|
631
|
+
})
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
this.abortController.signal.addEventListener("abort", abortListener, { once: true });
|
|
636
|
+
});
|
|
637
|
+
let timeoutPromise;
|
|
638
|
+
if (this.timeoutMs !== void 0) {
|
|
639
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
640
|
+
this.timeoutTimerId = setTimeout(() => {
|
|
641
|
+
if (this._state !== "running" /* RUNNING */) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
645
|
+
const timeoutError = new AhkoTimeoutError(
|
|
646
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
647
|
+
{ timeoutMs: this.timeoutMs }
|
|
648
|
+
);
|
|
649
|
+
this.abortController.abort(timeoutError);
|
|
650
|
+
reject(timeoutError);
|
|
651
|
+
}, this.timeoutMs);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
let taskExecutionPromise;
|
|
655
|
+
try {
|
|
656
|
+
taskExecutionPromise = Promise.resolve(this.task(context));
|
|
657
|
+
} catch (syncError) {
|
|
658
|
+
taskExecutionPromise = Promise.reject(syncError);
|
|
659
|
+
}
|
|
660
|
+
taskExecutionPromise.catch(() => {
|
|
661
|
+
});
|
|
662
|
+
const racePromises = [
|
|
663
|
+
taskExecutionPromise,
|
|
664
|
+
abortPromise
|
|
665
|
+
];
|
|
666
|
+
if (timeoutPromise) {
|
|
667
|
+
racePromises.push(timeoutPromise);
|
|
668
|
+
}
|
|
435
669
|
try {
|
|
436
|
-
const result = await
|
|
670
|
+
const result = await Promise.race(racePromises);
|
|
671
|
+
this.clearTimeoutTimer();
|
|
672
|
+
if (abortListener) {
|
|
673
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
674
|
+
}
|
|
675
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
676
|
+
throw new AhkoTimeoutError(
|
|
677
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
678
|
+
{ timeoutMs: this.timeoutMs }
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
if (this._state === "cancelled" /* CANCELLED */) {
|
|
682
|
+
throw new AhkoCancellationError("Task was cancelled during execution");
|
|
683
|
+
}
|
|
437
684
|
this._state = "completed" /* COMPLETED */;
|
|
438
|
-
this.cleanup();
|
|
439
685
|
return result;
|
|
440
686
|
} catch (error) {
|
|
441
|
-
this.
|
|
442
|
-
|
|
687
|
+
this.clearTimeoutTimer();
|
|
688
|
+
if (abortListener) {
|
|
689
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
690
|
+
}
|
|
691
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
692
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
693
|
+
if (error instanceof AhkoTimeoutError) {
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
throw new AhkoTimeoutError(
|
|
697
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
698
|
+
{
|
|
699
|
+
timeoutMs: this.timeoutMs,
|
|
700
|
+
cause: error instanceof Error ? error : void 0
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const isCancelled = this._state === "cancelled" /* CANCELLED */ || this.abortController.signal.aborted || (this.externalSignal?.aborted ?? false);
|
|
443
705
|
if (isCancelled) {
|
|
444
706
|
this._state = "cancelled" /* CANCELLED */;
|
|
707
|
+
if (error instanceof AhkoCancellationError) {
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
445
710
|
throw new AhkoCancellationError("Task was cancelled during execution", {
|
|
446
711
|
cause: error instanceof Error ? error : void 0
|
|
447
712
|
});
|
|
@@ -450,6 +715,15 @@ var TaskRunner = class {
|
|
|
450
715
|
throw error;
|
|
451
716
|
}
|
|
452
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Clears the active timeout timer.
|
|
720
|
+
*/
|
|
721
|
+
clearTimeoutTimer() {
|
|
722
|
+
if (this.timeoutTimerId !== void 0) {
|
|
723
|
+
clearTimeout(this.timeoutTimerId);
|
|
724
|
+
this.timeoutTimerId = void 0;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
453
727
|
/**
|
|
454
728
|
* Cancels the task, aborting pending or running execution.
|
|
455
729
|
*
|
|
@@ -461,6 +735,7 @@ var TaskRunner = class {
|
|
|
461
735
|
}
|
|
462
736
|
const wasPending = this._state === "pending" /* PENDING */;
|
|
463
737
|
this._state = "cancelled" /* CANCELLED */;
|
|
738
|
+
this.clearTimeoutTimer();
|
|
464
739
|
this.abortController.abort(reason);
|
|
465
740
|
this.cleanup();
|
|
466
741
|
if (wasPending) {
|
|
@@ -482,6 +757,7 @@ var TaskRunner = class {
|
|
|
482
757
|
* Detaches event listeners from external signal to guarantee memory safety.
|
|
483
758
|
*/
|
|
484
759
|
cleanup() {
|
|
760
|
+
this.clearTimeoutTimer();
|
|
485
761
|
if (this.externalSignal && this.abortListener) {
|
|
486
762
|
this.externalSignal.removeEventListener("abort", this.abortListener);
|
|
487
763
|
}
|
|
@@ -533,7 +809,7 @@ var Ahko = class {
|
|
|
533
809
|
if (typeof task !== "function") {
|
|
534
810
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
535
811
|
}
|
|
536
|
-
const runner = new TaskRunner(task, options?.signal);
|
|
812
|
+
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
537
813
|
return this.queue.enqueue(runner, options);
|
|
538
814
|
}
|
|
539
815
|
/**
|
|
@@ -582,7 +858,7 @@ var Ahko = class {
|
|
|
582
858
|
};
|
|
583
859
|
|
|
584
860
|
// src/version.ts
|
|
585
|
-
var VERSION = "0.
|
|
861
|
+
var VERSION = "0.4.0";
|
|
586
862
|
|
|
587
863
|
// src/errors/queue.error.ts
|
|
588
864
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -599,20 +875,60 @@ var AhkoQueueError = class extends AhkoError {
|
|
|
599
875
|
}
|
|
600
876
|
};
|
|
601
877
|
|
|
602
|
-
// src/
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
878
|
+
// src/scheduler/signal.ts
|
|
879
|
+
function combineSignals(signals) {
|
|
880
|
+
const activeSignals = signals.filter(
|
|
881
|
+
(signal) => signal !== void 0
|
|
882
|
+
);
|
|
883
|
+
if (activeSignals.length === 0) {
|
|
884
|
+
const controller2 = new AbortController();
|
|
885
|
+
return {
|
|
886
|
+
signal: controller2.signal,
|
|
887
|
+
cleanup: () => {
|
|
888
|
+
}
|
|
889
|
+
};
|
|
614
890
|
}
|
|
615
|
-
|
|
891
|
+
const alreadyAborted = activeSignals.find((s) => s.aborted);
|
|
892
|
+
if (alreadyAborted) {
|
|
893
|
+
const controller2 = new AbortController();
|
|
894
|
+
controller2.abort(alreadyAborted.reason);
|
|
895
|
+
return {
|
|
896
|
+
signal: controller2.signal,
|
|
897
|
+
cleanup: () => {
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
if (activeSignals.length === 1) {
|
|
902
|
+
return {
|
|
903
|
+
signal: activeSignals[0],
|
|
904
|
+
cleanup: () => {
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const controller = new AbortController();
|
|
909
|
+
const cleanupFns = [];
|
|
910
|
+
const onAbort = (event) => {
|
|
911
|
+
const target = event.target;
|
|
912
|
+
cleanup();
|
|
913
|
+
controller.abort(target.reason);
|
|
914
|
+
};
|
|
915
|
+
for (const sig of activeSignals) {
|
|
916
|
+
sig.addEventListener("abort", onAbort, { once: true });
|
|
917
|
+
cleanupFns.push(() => {
|
|
918
|
+
sig.removeEventListener("abort", onAbort);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const cleanup = () => {
|
|
922
|
+
for (const fn of cleanupFns) {
|
|
923
|
+
fn();
|
|
924
|
+
}
|
|
925
|
+
cleanupFns.length = 0;
|
|
926
|
+
};
|
|
927
|
+
return {
|
|
928
|
+
signal: controller.signal,
|
|
929
|
+
cleanup
|
|
930
|
+
};
|
|
931
|
+
}
|
|
616
932
|
// Annotate the CommonJS export names for ESM import in node:
|
|
617
933
|
0 && (module.exports = {
|
|
618
934
|
Ahko,
|
|
@@ -621,8 +937,12 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
621
937
|
AhkoError,
|
|
622
938
|
AhkoQueueError,
|
|
623
939
|
AhkoTimeoutError,
|
|
940
|
+
DEFAULT_BASE_DELAY,
|
|
941
|
+
DEFAULT_MAX_DELAY,
|
|
624
942
|
EScheduleStrategy,
|
|
625
943
|
ETaskState,
|
|
626
|
-
VERSION
|
|
944
|
+
VERSION,
|
|
945
|
+
calculateBackoff,
|
|
946
|
+
combineSignals
|
|
627
947
|
});
|
|
628
948
|
//# sourceMappingURL=index.cjs.map
|