@mrjacket/ahko 0.3.0 → 0.5.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 +28 -0
- package/README.md +59 -3
- package/dist/ahko.d.ts +23 -7
- package/dist/errors/timeout.error.d.ts +15 -2
- package/dist/index.cjs +651 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +649 -54
- package/dist/index.js.map +1 -1
- package/dist/models/options.model.d.ts +24 -0
- package/dist/models/strategy.model.d.ts +6 -2
- package/dist/scheduler/debounce-coordinator.d.ts +41 -0
- package/dist/scheduler/signal.d.ts +20 -0
- package/dist/scheduler/task-queue.d.ts +16 -3
- package/dist/scheduler/task-runner.d.ts +17 -8
- package/dist/scheduler/throttle-coordinator.d.ts +43 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,7 +31,8 @@ __export(src_exports, {
|
|
|
31
31
|
EScheduleStrategy: () => EScheduleStrategy,
|
|
32
32
|
ETaskState: () => ETaskState,
|
|
33
33
|
VERSION: () => VERSION,
|
|
34
|
-
calculateBackoff: () => calculateBackoff
|
|
34
|
+
calculateBackoff: () => calculateBackoff,
|
|
35
|
+
combineSignals: () => combineSignals
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(src_exports);
|
|
37
38
|
|
|
@@ -70,9 +71,31 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
70
71
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
71
72
|
EScheduleStrategy2["DELAY"] = "delay";
|
|
72
73
|
EScheduleStrategy2["IDLE"] = "idle";
|
|
74
|
+
EScheduleStrategy2["THROTTLE"] = "throttle";
|
|
75
|
+
EScheduleStrategy2["DEBOUNCE"] = "debounce";
|
|
73
76
|
return EScheduleStrategy2;
|
|
74
77
|
})(EScheduleStrategy || {});
|
|
75
78
|
|
|
79
|
+
// src/errors/timeout.error.ts
|
|
80
|
+
var AhkoTimeoutError = class extends AhkoError {
|
|
81
|
+
/**
|
|
82
|
+
* The timeout threshold in milliseconds that was exceeded, if configured.
|
|
83
|
+
*/
|
|
84
|
+
timeoutMs;
|
|
85
|
+
/**
|
|
86
|
+
* Creates a new AhkoTimeoutError.
|
|
87
|
+
*
|
|
88
|
+
* @param message - Explanation of timeout expiry.
|
|
89
|
+
* @param options - Standard Error options including optional timeoutMs and cause.
|
|
90
|
+
*/
|
|
91
|
+
constructor(message = "Task execution timed out", options) {
|
|
92
|
+
super(message, options);
|
|
93
|
+
this.name = "AhkoTimeoutError";
|
|
94
|
+
this.timeoutMs = options?.timeoutMs;
|
|
95
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
76
99
|
// src/models/state.model.ts
|
|
77
100
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
78
101
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -109,6 +132,163 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
109
132
|
return Math.floor(cappedDelay);
|
|
110
133
|
}
|
|
111
134
|
|
|
135
|
+
// src/errors/cancellation.error.ts
|
|
136
|
+
var AhkoCancellationError = class extends AhkoError {
|
|
137
|
+
/**
|
|
138
|
+
* Creates a new AhkoCancellationError.
|
|
139
|
+
*
|
|
140
|
+
* @param message - Reason for cancellation.
|
|
141
|
+
* @param options - Standard Error options including cause.
|
|
142
|
+
*/
|
|
143
|
+
constructor(message = "Task was cancelled", options) {
|
|
144
|
+
super(message, options);
|
|
145
|
+
this.name = "AhkoCancellationError";
|
|
146
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/scheduler/debounce-coordinator.ts
|
|
151
|
+
var DebounceCoordinator = class {
|
|
152
|
+
entries = /* @__PURE__ */ new Map();
|
|
153
|
+
/**
|
|
154
|
+
* Schedules a task under the debounce strategy.
|
|
155
|
+
*
|
|
156
|
+
* @param key - Explicit identity key.
|
|
157
|
+
* @param task - Work to execute once calls stop arriving.
|
|
158
|
+
* @param waitMs - Quiet window duration in milliseconds.
|
|
159
|
+
* @param options - Scheduling options.
|
|
160
|
+
* @param dispatchFn - Callback invoked when the debounce window expires to dispatch the task to the queue.
|
|
161
|
+
* @returns Shared promise that resolves/rejects with the final execution outcome.
|
|
162
|
+
*/
|
|
163
|
+
schedule(key, task, waitMs, options, dispatchFn) {
|
|
164
|
+
const existing = this.entries.get(key);
|
|
165
|
+
if (existing) {
|
|
166
|
+
clearTimeout(existing.timerId);
|
|
167
|
+
if (existing.options?.signal && existing.abortListener) {
|
|
168
|
+
existing.options.signal.removeEventListener("abort", existing.abortListener);
|
|
169
|
+
}
|
|
170
|
+
existing.task = task;
|
|
171
|
+
existing.options = options;
|
|
172
|
+
if (options?.signal?.aborted) {
|
|
173
|
+
this.entries.delete(key);
|
|
174
|
+
const err = new AhkoCancellationError(
|
|
175
|
+
typeof options.signal.reason === "string" ? options.signal.reason : "Debounced task was cancelled prior to execution",
|
|
176
|
+
{ cause: options.signal.reason instanceof Error ? options.signal.reason : void 0 }
|
|
177
|
+
);
|
|
178
|
+
existing.reject(err);
|
|
179
|
+
return existing.promise;
|
|
180
|
+
}
|
|
181
|
+
if (options?.signal) {
|
|
182
|
+
const listener = () => {
|
|
183
|
+
this.cancel(key, options.signal?.reason);
|
|
184
|
+
};
|
|
185
|
+
existing.abortListener = listener;
|
|
186
|
+
options.signal.addEventListener("abort", listener, { once: true });
|
|
187
|
+
}
|
|
188
|
+
existing.timerId = setTimeout(() => {
|
|
189
|
+
void this.flush(key, dispatchFn);
|
|
190
|
+
}, waitMs);
|
|
191
|
+
return existing.promise;
|
|
192
|
+
}
|
|
193
|
+
let resolvePromise;
|
|
194
|
+
let rejectPromise;
|
|
195
|
+
const promise = new Promise((resolve, reject) => {
|
|
196
|
+
resolvePromise = resolve;
|
|
197
|
+
rejectPromise = reject;
|
|
198
|
+
});
|
|
199
|
+
if (options?.signal?.aborted) {
|
|
200
|
+
const err = new AhkoCancellationError(
|
|
201
|
+
typeof options.signal.reason === "string" ? options.signal.reason : "Debounced task was cancelled prior to execution",
|
|
202
|
+
{ cause: options.signal.reason instanceof Error ? options.signal.reason : void 0 }
|
|
203
|
+
);
|
|
204
|
+
rejectPromise(err);
|
|
205
|
+
return promise;
|
|
206
|
+
}
|
|
207
|
+
let abortListener;
|
|
208
|
+
if (options?.signal) {
|
|
209
|
+
abortListener = () => {
|
|
210
|
+
this.cancel(key, options.signal?.reason);
|
|
211
|
+
};
|
|
212
|
+
options.signal.addEventListener("abort", abortListener, { once: true });
|
|
213
|
+
}
|
|
214
|
+
const timerId = setTimeout(() => {
|
|
215
|
+
void this.flush(key, dispatchFn);
|
|
216
|
+
}, waitMs);
|
|
217
|
+
const entry = {
|
|
218
|
+
key,
|
|
219
|
+
task,
|
|
220
|
+
options,
|
|
221
|
+
timerId,
|
|
222
|
+
resolve: resolvePromise,
|
|
223
|
+
reject: rejectPromise,
|
|
224
|
+
promise,
|
|
225
|
+
abortListener
|
|
226
|
+
};
|
|
227
|
+
this.entries.set(key, entry);
|
|
228
|
+
return promise;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Dispatches the coalesced task when the quiet window expires.
|
|
232
|
+
*/
|
|
233
|
+
async flush(key, dispatchFn) {
|
|
234
|
+
const entry = this.entries.get(key);
|
|
235
|
+
if (!entry) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.entries.delete(key);
|
|
239
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
240
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = await dispatchFn(entry.task, entry.options);
|
|
244
|
+
entry.resolve(result);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
entry.reject(error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Cancels a pending debounced task by key.
|
|
251
|
+
*
|
|
252
|
+
* @param key - Identity key to cancel.
|
|
253
|
+
* @param reason - Optional cancellation reason.
|
|
254
|
+
*/
|
|
255
|
+
cancel(key, reason) {
|
|
256
|
+
const entry = this.entries.get(key);
|
|
257
|
+
if (!entry) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
clearTimeout(entry.timerId);
|
|
261
|
+
this.entries.delete(key);
|
|
262
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
263
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
264
|
+
}
|
|
265
|
+
const cancelError = new AhkoCancellationError(
|
|
266
|
+
typeof reason === "string" ? reason : "Debounced task was cancelled prior to execution",
|
|
267
|
+
{ cause: reason instanceof Error ? reason : void 0 }
|
|
268
|
+
);
|
|
269
|
+
entry.reject(cancelError);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Number of pending debounced tasks waiting for quiet window expiry.
|
|
273
|
+
*/
|
|
274
|
+
get size() {
|
|
275
|
+
return this.entries.size;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Cancels all pending debounced entries and clears the map.
|
|
279
|
+
*/
|
|
280
|
+
clear() {
|
|
281
|
+
for (const [key, entry] of this.entries) {
|
|
282
|
+
clearTimeout(entry.timerId);
|
|
283
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
284
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
285
|
+
}
|
|
286
|
+
entry.reject(new AhkoCancellationError("Debounced tasks cleared"));
|
|
287
|
+
}
|
|
288
|
+
this.entries.clear();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
112
292
|
// src/scheduler/idle-scheduler.ts
|
|
113
293
|
var IdleScheduler = class {
|
|
114
294
|
/**
|
|
@@ -148,10 +328,150 @@ var IdleScheduler = class {
|
|
|
148
328
|
}
|
|
149
329
|
};
|
|
150
330
|
|
|
331
|
+
// src/scheduler/throttle-coordinator.ts
|
|
332
|
+
var ThrottleCoordinator = class {
|
|
333
|
+
entries = /* @__PURE__ */ new Map();
|
|
334
|
+
/**
|
|
335
|
+
* Schedules a task under the throttle strategy.
|
|
336
|
+
*
|
|
337
|
+
* @param key - Explicit identity key.
|
|
338
|
+
* @param task - Work to execute.
|
|
339
|
+
* @param waitMs - Throttle interval duration in milliseconds.
|
|
340
|
+
* @param options - Scheduling options.
|
|
341
|
+
* @param dispatchFn - Callback invoked to dispatch task execution into the queue.
|
|
342
|
+
* @returns Promise resolving with the leading execution or coalesced trailing result.
|
|
343
|
+
*/
|
|
344
|
+
schedule(key, task, waitMs, options, dispatchFn) {
|
|
345
|
+
const existing = this.entries.get(key);
|
|
346
|
+
if (!existing) {
|
|
347
|
+
const entry = {
|
|
348
|
+
key
|
|
349
|
+
};
|
|
350
|
+
entry.windowTimerId = setTimeout(() => {
|
|
351
|
+
void this.onWindowExpire(key, waitMs, dispatchFn);
|
|
352
|
+
}, waitMs);
|
|
353
|
+
this.entries.set(key, entry);
|
|
354
|
+
return dispatchFn(task, options);
|
|
355
|
+
}
|
|
356
|
+
existing.trailingTask = task;
|
|
357
|
+
existing.trailingOptions = options;
|
|
358
|
+
if (existing.trailingPromise) {
|
|
359
|
+
return existing.trailingPromise;
|
|
360
|
+
}
|
|
361
|
+
let resolvePromise;
|
|
362
|
+
let rejectPromise;
|
|
363
|
+
existing.trailingPromise = new Promise((resolve, reject) => {
|
|
364
|
+
resolvePromise = resolve;
|
|
365
|
+
rejectPromise = reject;
|
|
366
|
+
});
|
|
367
|
+
existing.trailingResolve = resolvePromise;
|
|
368
|
+
existing.trailingReject = rejectPromise;
|
|
369
|
+
if (options?.signal) {
|
|
370
|
+
const listener = () => {
|
|
371
|
+
if (existing.trailingReject) {
|
|
372
|
+
existing.trailingReject(
|
|
373
|
+
new AhkoCancellationError("Throttled trailing task was cancelled", {
|
|
374
|
+
cause: options.signal?.reason instanceof Error ? options.signal.reason : void 0
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
existing.trailingTask = void 0;
|
|
378
|
+
existing.trailingOptions = void 0;
|
|
379
|
+
existing.trailingPromise = void 0;
|
|
380
|
+
existing.trailingResolve = void 0;
|
|
381
|
+
existing.trailingReject = void 0;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
existing.abortListener = listener;
|
|
385
|
+
options.signal.addEventListener("abort", listener, { once: true });
|
|
386
|
+
}
|
|
387
|
+
return existing.trailingPromise;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Invoked when the throttle interval window timer expires.
|
|
391
|
+
*/
|
|
392
|
+
async onWindowExpire(key, waitMs, dispatchFn) {
|
|
393
|
+
const entry = this.entries.get(key);
|
|
394
|
+
if (!entry) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (entry.trailingTask) {
|
|
398
|
+
const task = entry.trailingTask;
|
|
399
|
+
const options = entry.trailingOptions;
|
|
400
|
+
const resolve = entry.trailingResolve;
|
|
401
|
+
const reject = entry.trailingReject;
|
|
402
|
+
entry.trailingTask = void 0;
|
|
403
|
+
entry.trailingOptions = void 0;
|
|
404
|
+
entry.trailingPromise = void 0;
|
|
405
|
+
entry.trailingResolve = void 0;
|
|
406
|
+
entry.trailingReject = void 0;
|
|
407
|
+
entry.windowTimerId = setTimeout(() => {
|
|
408
|
+
void this.onWindowExpire(key, waitMs, dispatchFn);
|
|
409
|
+
}, waitMs);
|
|
410
|
+
try {
|
|
411
|
+
const result = await dispatchFn(task, options);
|
|
412
|
+
resolve?.(result);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
reject?.(error);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.entries.delete(key);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Cancels any pending trailing throttled task for a given key.
|
|
422
|
+
*
|
|
423
|
+
* @param key - Identity key to cancel.
|
|
424
|
+
* @param reason - Optional cancellation reason.
|
|
425
|
+
*/
|
|
426
|
+
cancel(key, reason) {
|
|
427
|
+
const entry = this.entries.get(key);
|
|
428
|
+
if (!entry) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (entry.windowTimerId !== void 0) {
|
|
432
|
+
clearTimeout(entry.windowTimerId);
|
|
433
|
+
}
|
|
434
|
+
this.entries.delete(key);
|
|
435
|
+
if (entry.trailingReject) {
|
|
436
|
+
const cancelError = new AhkoCancellationError(
|
|
437
|
+
typeof reason === "string" ? reason : "Throttled task was cancelled",
|
|
438
|
+
{ cause: reason instanceof Error ? reason : void 0 }
|
|
439
|
+
);
|
|
440
|
+
entry.trailingReject(cancelError);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Number of keys currently actively throttled.
|
|
445
|
+
*/
|
|
446
|
+
get size() {
|
|
447
|
+
return this.entries.size;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Clears all throttled entries and timers.
|
|
451
|
+
*/
|
|
452
|
+
clear() {
|
|
453
|
+
for (const [key, entry] of this.entries) {
|
|
454
|
+
if (entry.windowTimerId !== void 0) {
|
|
455
|
+
clearTimeout(entry.windowTimerId);
|
|
456
|
+
}
|
|
457
|
+
if (entry.trailingReject) {
|
|
458
|
+
entry.trailingReject(new AhkoCancellationError("Throttled tasks cleared"));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this.entries.clear();
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
151
465
|
// src/scheduler/task-queue.ts
|
|
152
466
|
var TaskQueue = class {
|
|
153
467
|
/** Maximum concurrent active tasks */
|
|
154
468
|
concurrency;
|
|
469
|
+
/** Minimum interval in milliseconds between consecutive task starts */
|
|
470
|
+
minIntervalMs;
|
|
471
|
+
/** Timestamp of the most recent task start */
|
|
472
|
+
lastTaskStartTime = 0;
|
|
473
|
+
/** Active rate limit timer for pacing consecutive tasks */
|
|
474
|
+
rateLimitTimer;
|
|
155
475
|
/** Queue of pending task runners waiting for a concurrency slot */
|
|
156
476
|
queue = [];
|
|
157
477
|
/** Set of task runners currently executing */
|
|
@@ -162,6 +482,10 @@ var TaskQueue = class {
|
|
|
162
482
|
idleEntries = /* @__PURE__ */ new Set();
|
|
163
483
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
164
484
|
retryEntries = /* @__PURE__ */ new Set();
|
|
485
|
+
/** Coordinator for debounced tasks with key coalescing */
|
|
486
|
+
debounceCoordinator = new DebounceCoordinator();
|
|
487
|
+
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
488
|
+
throttleCoordinator = new ThrottleCoordinator();
|
|
165
489
|
/** WeakMap associating task runners with their scheduling options */
|
|
166
490
|
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
167
491
|
/** Cumulative completed tasks counter */
|
|
@@ -176,15 +500,22 @@ var TaskQueue = class {
|
|
|
176
500
|
* Creates a new TaskQueue.
|
|
177
501
|
*
|
|
178
502
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
179
|
-
* @
|
|
503
|
+
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
504
|
+
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
180
505
|
*/
|
|
181
|
-
constructor(concurrency = Infinity) {
|
|
506
|
+
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
182
507
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
183
508
|
throw new AhkoConfigurationError(
|
|
184
509
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
185
510
|
);
|
|
186
511
|
}
|
|
512
|
+
if (typeof minIntervalMs !== "number" || Number.isNaN(minIntervalMs) || !Number.isFinite(minIntervalMs) || minIntervalMs < 0) {
|
|
513
|
+
throw new AhkoConfigurationError(
|
|
514
|
+
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
187
517
|
this.concurrency = concurrency;
|
|
518
|
+
this.minIntervalMs = minIntervalMs;
|
|
188
519
|
}
|
|
189
520
|
/**
|
|
190
521
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -197,11 +528,24 @@ var TaskQueue = class {
|
|
|
197
528
|
*/
|
|
198
529
|
enqueue(runner, options) {
|
|
199
530
|
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
200
|
-
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */) {
|
|
531
|
+
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */ && strategy !== "throttle" /* THROTTLE */ && strategy !== "debounce" /* DEBOUNCE */) {
|
|
201
532
|
throw new AhkoConfigurationError(
|
|
202
|
-
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
533
|
+
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle", "throttle", "debounce".`
|
|
203
534
|
);
|
|
204
535
|
}
|
|
536
|
+
if (strategy === "throttle" /* THROTTLE */ || strategy === "debounce" /* DEBOUNCE */) {
|
|
537
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
538
|
+
throw new AhkoConfigurationError(
|
|
539
|
+
`Strategy "${strategy}" requires a valid "key" of type string or symbol.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
543
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
544
|
+
throw new AhkoConfigurationError(
|
|
545
|
+
`Strategy "${strategy}" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
205
549
|
if (options?.retry) {
|
|
206
550
|
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
207
551
|
throw new AhkoConfigurationError(
|
|
@@ -219,6 +563,13 @@ var TaskQueue = class {
|
|
|
219
563
|
);
|
|
220
564
|
}
|
|
221
565
|
}
|
|
566
|
+
if (options?.timeoutMs !== void 0) {
|
|
567
|
+
if (typeof options.timeoutMs !== "number" || Number.isNaN(options.timeoutMs) || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
568
|
+
throw new AhkoConfigurationError(
|
|
569
|
+
`Invalid timeoutMs "${options.timeoutMs}". timeoutMs must be a positive finite number greater than 0.`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
222
573
|
if (options) {
|
|
223
574
|
this.runnerOptions.set(runner, options);
|
|
224
575
|
}
|
|
@@ -321,10 +672,41 @@ var TaskQueue = class {
|
|
|
321
672
|
}
|
|
322
673
|
/**
|
|
323
674
|
* Pumps the queue by picking pending tasks and executing them
|
|
324
|
-
* as long as concurrency capacity is available.
|
|
675
|
+
* as long as concurrency capacity is available and minIntervalMs is respected.
|
|
325
676
|
*/
|
|
326
677
|
pump() {
|
|
678
|
+
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
682
|
+
const now = Date.now();
|
|
683
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
684
|
+
if (elapsed < this.minIntervalMs) {
|
|
685
|
+
if (this.rateLimitTimer === void 0) {
|
|
686
|
+
const delay = this.minIntervalMs - elapsed;
|
|
687
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
688
|
+
this.rateLimitTimer = void 0;
|
|
689
|
+
this.pump();
|
|
690
|
+
}, delay);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
327
695
|
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
696
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
697
|
+
const now = Date.now();
|
|
698
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
699
|
+
if (elapsed < this.minIntervalMs) {
|
|
700
|
+
if (this.rateLimitTimer === void 0) {
|
|
701
|
+
const delay = this.minIntervalMs - elapsed;
|
|
702
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
703
|
+
this.rateLimitTimer = void 0;
|
|
704
|
+
this.pump();
|
|
705
|
+
}, delay);
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
328
710
|
const runner = this.queue.shift();
|
|
329
711
|
if (!runner) {
|
|
330
712
|
break;
|
|
@@ -333,7 +715,19 @@ var TaskQueue = class {
|
|
|
333
715
|
continue;
|
|
334
716
|
}
|
|
335
717
|
this.activeRunners.add(runner);
|
|
718
|
+
this.lastTaskStartTime = Date.now();
|
|
336
719
|
void this.executeRunner(runner);
|
|
720
|
+
if (this.minIntervalMs > 0) {
|
|
721
|
+
if (this.queue.length > 0 && this.activeRunners.size < this.concurrency) {
|
|
722
|
+
if (this.rateLimitTimer === void 0) {
|
|
723
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
724
|
+
this.rateLimitTimer = void 0;
|
|
725
|
+
this.pump();
|
|
726
|
+
}, this.minIntervalMs);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
337
731
|
}
|
|
338
732
|
}
|
|
339
733
|
/**
|
|
@@ -361,7 +755,7 @@ var TaskQueue = class {
|
|
|
361
755
|
this.scheduleRetry(runner, options);
|
|
362
756
|
return;
|
|
363
757
|
}
|
|
364
|
-
if (runner.state === "timed_out" /* TIMED_OUT */) {
|
|
758
|
+
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
365
759
|
this.timedOutTasks++;
|
|
366
760
|
} else {
|
|
367
761
|
this.failedTasks++;
|
|
@@ -426,7 +820,7 @@ var TaskQueue = class {
|
|
|
426
820
|
getStats() {
|
|
427
821
|
return Object.freeze({
|
|
428
822
|
activeTasks: this.activeRunners.size,
|
|
429
|
-
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size,
|
|
823
|
+
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size + this.debounceCoordinator.size + this.throttleCoordinator.size,
|
|
430
824
|
completedTasks: this.completedTasks,
|
|
431
825
|
failedTasks: this.failedTasks,
|
|
432
826
|
cancelledTasks: this.cancelledTasks,
|
|
@@ -436,21 +830,6 @@ var TaskQueue = class {
|
|
|
436
830
|
}
|
|
437
831
|
};
|
|
438
832
|
|
|
439
|
-
// src/errors/cancellation.error.ts
|
|
440
|
-
var AhkoCancellationError = class extends AhkoError {
|
|
441
|
-
/**
|
|
442
|
-
* Creates a new AhkoCancellationError.
|
|
443
|
-
*
|
|
444
|
-
* @param message - Reason for cancellation.
|
|
445
|
-
* @param options - Standard Error options including cause.
|
|
446
|
-
*/
|
|
447
|
-
constructor(message = "Task was cancelled", options) {
|
|
448
|
-
super(message, options);
|
|
449
|
-
this.name = "AhkoCancellationError";
|
|
450
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
|
|
454
833
|
// src/scheduler/task-runner.ts
|
|
455
834
|
var taskIdCounter = 0;
|
|
456
835
|
var TaskRunner = class {
|
|
@@ -464,6 +843,10 @@ var TaskRunner = class {
|
|
|
464
843
|
task;
|
|
465
844
|
/** User-supplied AbortSignal for external cancellation */
|
|
466
845
|
externalSignal;
|
|
846
|
+
/** Maximum execution duration allowed in milliseconds */
|
|
847
|
+
timeoutMs;
|
|
848
|
+
/** Active timeout timer identifier */
|
|
849
|
+
timeoutTimerId;
|
|
467
850
|
/** Abort event listener reference for clean detachment */
|
|
468
851
|
abortListener;
|
|
469
852
|
/** Promise resolve handler */
|
|
@@ -474,16 +857,20 @@ var TaskRunner = class {
|
|
|
474
857
|
promise;
|
|
475
858
|
/** Callback invoked when runner is cancelled while pending */
|
|
476
859
|
onCancel;
|
|
860
|
+
/** Current execution attempt count (1-indexed) */
|
|
861
|
+
attempt = 1;
|
|
477
862
|
/**
|
|
478
863
|
* Creates a new TaskRunner instance.
|
|
479
864
|
*
|
|
480
865
|
* @param task - The asynchronous work unit to run.
|
|
481
866
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
867
|
+
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
482
868
|
*/
|
|
483
|
-
constructor(task, externalSignal) {
|
|
869
|
+
constructor(task, externalSignal, timeoutMs) {
|
|
484
870
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
485
871
|
this.task = task;
|
|
486
872
|
this.externalSignal = externalSignal;
|
|
873
|
+
this.timeoutMs = timeoutMs;
|
|
487
874
|
this.abortController = new AbortController();
|
|
488
875
|
this.promise = new Promise((resolve, reject) => {
|
|
489
876
|
this.resolvePromise = resolve;
|
|
@@ -497,6 +884,7 @@ var TaskRunner = class {
|
|
|
497
884
|
typeof reason === "string" ? reason : "Task was cancelled prior to execution",
|
|
498
885
|
{ cause: reason instanceof Error ? reason : void 0 }
|
|
499
886
|
);
|
|
887
|
+
this.abortController.abort(cancelError);
|
|
500
888
|
this.rejectPromise(cancelError);
|
|
501
889
|
} else {
|
|
502
890
|
this.abortListener = () => {
|
|
@@ -512,8 +900,6 @@ var TaskRunner = class {
|
|
|
512
900
|
get state() {
|
|
513
901
|
return this._state;
|
|
514
902
|
}
|
|
515
|
-
/** Current execution attempt count (1-indexed) */
|
|
516
|
-
attempt = 1;
|
|
517
903
|
/**
|
|
518
904
|
* Resolves the deferred promise.
|
|
519
905
|
*
|
|
@@ -533,14 +919,14 @@ var TaskRunner = class {
|
|
|
533
919
|
this.rejectPromise(reason);
|
|
534
920
|
}
|
|
535
921
|
/**
|
|
536
|
-
* Evaluates if the task should be retried following an execution failure.
|
|
922
|
+
* Evaluates if the task should be retried following an execution failure or timeout.
|
|
537
923
|
*
|
|
538
924
|
* @param error - The error encountered during the attempt.
|
|
539
925
|
* @param retryOptions - Configured retry policy.
|
|
540
926
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
541
927
|
*/
|
|
542
928
|
async canRetry(error, retryOptions) {
|
|
543
|
-
if (this._state === "cancelled" /* CANCELLED */ || this.
|
|
929
|
+
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
544
930
|
return false;
|
|
545
931
|
}
|
|
546
932
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -561,12 +947,13 @@ var TaskRunner = class {
|
|
|
561
947
|
}
|
|
562
948
|
this.attempt++;
|
|
563
949
|
this._state = "pending" /* PENDING */;
|
|
950
|
+
this.abortController = new AbortController();
|
|
564
951
|
return true;
|
|
565
952
|
}
|
|
566
953
|
/**
|
|
567
954
|
* Executes the task within an allocated concurrency slot.
|
|
568
955
|
*
|
|
569
|
-
* @returns A promise resolving to the task result or rejecting on failure/cancellation.
|
|
956
|
+
* @returns A promise resolving to the task result or rejecting on failure/cancellation/timeout.
|
|
570
957
|
*/
|
|
571
958
|
async run() {
|
|
572
959
|
if (this._state === "cancelled" /* CANCELLED */) {
|
|
@@ -577,14 +964,100 @@ var TaskRunner = class {
|
|
|
577
964
|
signal: this.abortController.signal,
|
|
578
965
|
taskId: this.taskId
|
|
579
966
|
};
|
|
967
|
+
let abortListener;
|
|
968
|
+
const abortPromise = new Promise((_, reject) => {
|
|
969
|
+
abortListener = () => {
|
|
970
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
971
|
+
reject(
|
|
972
|
+
new AhkoTimeoutError(
|
|
973
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
974
|
+
{ timeoutMs: this.timeoutMs }
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
const reason = this.abortController.signal.reason;
|
|
979
|
+
reject(
|
|
980
|
+
new AhkoCancellationError("Task was cancelled during execution", {
|
|
981
|
+
cause: reason instanceof Error ? reason : void 0
|
|
982
|
+
})
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
this.abortController.signal.addEventListener("abort", abortListener, { once: true });
|
|
987
|
+
});
|
|
988
|
+
let timeoutPromise;
|
|
989
|
+
if (this.timeoutMs !== void 0) {
|
|
990
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
991
|
+
this.timeoutTimerId = setTimeout(() => {
|
|
992
|
+
if (this._state !== "running" /* RUNNING */) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
996
|
+
const timeoutError = new AhkoTimeoutError(
|
|
997
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
998
|
+
{ timeoutMs: this.timeoutMs }
|
|
999
|
+
);
|
|
1000
|
+
this.abortController.abort(timeoutError);
|
|
1001
|
+
reject(timeoutError);
|
|
1002
|
+
}, this.timeoutMs);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
let taskExecutionPromise;
|
|
1006
|
+
try {
|
|
1007
|
+
taskExecutionPromise = Promise.resolve(this.task(context));
|
|
1008
|
+
} catch (syncError) {
|
|
1009
|
+
taskExecutionPromise = Promise.reject(syncError);
|
|
1010
|
+
}
|
|
1011
|
+
taskExecutionPromise.catch(() => {
|
|
1012
|
+
});
|
|
1013
|
+
const racePromises = [
|
|
1014
|
+
taskExecutionPromise,
|
|
1015
|
+
abortPromise
|
|
1016
|
+
];
|
|
1017
|
+
if (timeoutPromise) {
|
|
1018
|
+
racePromises.push(timeoutPromise);
|
|
1019
|
+
}
|
|
580
1020
|
try {
|
|
581
|
-
const result = await
|
|
1021
|
+
const result = await Promise.race(racePromises);
|
|
1022
|
+
this.clearTimeoutTimer();
|
|
1023
|
+
if (abortListener) {
|
|
1024
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
1025
|
+
}
|
|
1026
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
1027
|
+
throw new AhkoTimeoutError(
|
|
1028
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1029
|
+
{ timeoutMs: this.timeoutMs }
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
if (this._state === "cancelled" /* CANCELLED */) {
|
|
1033
|
+
throw new AhkoCancellationError("Task was cancelled during execution");
|
|
1034
|
+
}
|
|
582
1035
|
this._state = "completed" /* COMPLETED */;
|
|
583
1036
|
return result;
|
|
584
1037
|
} catch (error) {
|
|
585
|
-
|
|
1038
|
+
this.clearTimeoutTimer();
|
|
1039
|
+
if (abortListener) {
|
|
1040
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
1041
|
+
}
|
|
1042
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
1043
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1044
|
+
if (error instanceof AhkoTimeoutError) {
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
throw new AhkoTimeoutError(
|
|
1048
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1049
|
+
{
|
|
1050
|
+
timeoutMs: this.timeoutMs,
|
|
1051
|
+
cause: error instanceof Error ? error : void 0
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const isCancelled = this._state === "cancelled" /* CANCELLED */ || this.abortController.signal.aborted || (this.externalSignal?.aborted ?? false);
|
|
586
1056
|
if (isCancelled) {
|
|
587
1057
|
this._state = "cancelled" /* CANCELLED */;
|
|
1058
|
+
if (error instanceof AhkoCancellationError) {
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
588
1061
|
throw new AhkoCancellationError("Task was cancelled during execution", {
|
|
589
1062
|
cause: error instanceof Error ? error : void 0
|
|
590
1063
|
});
|
|
@@ -593,6 +1066,15 @@ var TaskRunner = class {
|
|
|
593
1066
|
throw error;
|
|
594
1067
|
}
|
|
595
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Clears the active timeout timer.
|
|
1071
|
+
*/
|
|
1072
|
+
clearTimeoutTimer() {
|
|
1073
|
+
if (this.timeoutTimerId !== void 0) {
|
|
1074
|
+
clearTimeout(this.timeoutTimerId);
|
|
1075
|
+
this.timeoutTimerId = void 0;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
596
1078
|
/**
|
|
597
1079
|
* Cancels the task, aborting pending or running execution.
|
|
598
1080
|
*
|
|
@@ -604,6 +1086,7 @@ var TaskRunner = class {
|
|
|
604
1086
|
}
|
|
605
1087
|
const wasPending = this._state === "pending" /* PENDING */;
|
|
606
1088
|
this._state = "cancelled" /* CANCELLED */;
|
|
1089
|
+
this.clearTimeoutTimer();
|
|
607
1090
|
this.abortController.abort(reason);
|
|
608
1091
|
this.cleanup();
|
|
609
1092
|
if (wasPending) {
|
|
@@ -625,6 +1108,7 @@ var TaskRunner = class {
|
|
|
625
1108
|
* Detaches event listeners from external signal to guarantee memory safety.
|
|
626
1109
|
*/
|
|
627
1110
|
cleanup() {
|
|
1111
|
+
this.clearTimeoutTimer();
|
|
628
1112
|
if (this.externalSignal && this.abortListener) {
|
|
629
1113
|
this.externalSignal.removeEventListener("abort", this.abortListener);
|
|
630
1114
|
}
|
|
@@ -647,7 +1131,7 @@ var Ahko = class {
|
|
|
647
1131
|
* ```
|
|
648
1132
|
*/
|
|
649
1133
|
constructor(options) {
|
|
650
|
-
this.queue = new TaskQueue(options?.concurrency);
|
|
1134
|
+
this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
|
|
651
1135
|
}
|
|
652
1136
|
/**
|
|
653
1137
|
* Schedules a task for execution with full return type inference.
|
|
@@ -659,6 +1143,7 @@ var Ahko = class {
|
|
|
659
1143
|
*
|
|
660
1144
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
661
1145
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1146
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
662
1147
|
*
|
|
663
1148
|
* @example
|
|
664
1149
|
* ```typescript
|
|
@@ -676,7 +1161,48 @@ var Ahko = class {
|
|
|
676
1161
|
if (typeof task !== "function") {
|
|
677
1162
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
678
1163
|
}
|
|
679
|
-
const
|
|
1164
|
+
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1165
|
+
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1166
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1167
|
+
throw new AhkoConfigurationError(
|
|
1168
|
+
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1172
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1173
|
+
throw new AhkoConfigurationError(
|
|
1174
|
+
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return this.queue.debounceCoordinator.schedule(
|
|
1178
|
+
options.key,
|
|
1179
|
+
task,
|
|
1180
|
+
waitMs,
|
|
1181
|
+
options,
|
|
1182
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (strategy === "throttle" /* THROTTLE */) {
|
|
1186
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1187
|
+
throw new AhkoConfigurationError(
|
|
1188
|
+
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1192
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1193
|
+
throw new AhkoConfigurationError(
|
|
1194
|
+
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
return this.queue.throttleCoordinator.schedule(
|
|
1198
|
+
options.key,
|
|
1199
|
+
task,
|
|
1200
|
+
waitMs,
|
|
1201
|
+
options,
|
|
1202
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
680
1206
|
return this.queue.enqueue(runner, options);
|
|
681
1207
|
}
|
|
682
1208
|
/**
|
|
@@ -694,13 +1220,6 @@ var Ahko = class {
|
|
|
694
1220
|
*
|
|
695
1221
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
696
1222
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
697
|
-
*
|
|
698
|
-
* @example
|
|
699
|
-
* ```typescript
|
|
700
|
-
* const result = await ahko.idle(async ({ signal }) => {
|
|
701
|
-
* return computeAnalytics();
|
|
702
|
-
* });
|
|
703
|
-
* ```
|
|
704
1223
|
*/
|
|
705
1224
|
idle(task, options) {
|
|
706
1225
|
return this.schedule(task, {
|
|
@@ -708,6 +1227,42 @@ var Ahko = class {
|
|
|
708
1227
|
strategy: "idle" /* IDLE */
|
|
709
1228
|
});
|
|
710
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Convenience method to schedule a debounced task with key-based Promise coalescing.
|
|
1232
|
+
*
|
|
1233
|
+
* @template T - Inferred return type of the task.
|
|
1234
|
+
* @param key - Explicit identity key.
|
|
1235
|
+
* @param task - Work to execute once calls stop arriving.
|
|
1236
|
+
* @param waitMs - Quiet window duration in milliseconds.
|
|
1237
|
+
* @param options - Additional schedule options.
|
|
1238
|
+
* @returns Shared promise resolving with the final execution outcome.
|
|
1239
|
+
*/
|
|
1240
|
+
debounce(key, task, waitMs, options) {
|
|
1241
|
+
return this.schedule(task, {
|
|
1242
|
+
...options,
|
|
1243
|
+
strategy: "debounce" /* DEBOUNCE */,
|
|
1244
|
+
key,
|
|
1245
|
+
waitMs
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Convenience method to schedule a throttled task with leading execution and coalesced trailing run.
|
|
1250
|
+
*
|
|
1251
|
+
* @template T - Inferred return type of the task.
|
|
1252
|
+
* @param key - Explicit identity key.
|
|
1253
|
+
* @param task - Work to execute.
|
|
1254
|
+
* @param waitMs - Throttle interval duration in milliseconds.
|
|
1255
|
+
* @param options - Additional schedule options.
|
|
1256
|
+
* @returns Promise resolving with the leading or coalesced trailing result.
|
|
1257
|
+
*/
|
|
1258
|
+
throttle(key, task, waitMs, options) {
|
|
1259
|
+
return this.schedule(task, {
|
|
1260
|
+
...options,
|
|
1261
|
+
strategy: "throttle" /* THROTTLE */,
|
|
1262
|
+
key,
|
|
1263
|
+
waitMs
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
711
1266
|
/**
|
|
712
1267
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
713
1268
|
*
|
|
@@ -725,7 +1280,7 @@ var Ahko = class {
|
|
|
725
1280
|
};
|
|
726
1281
|
|
|
727
1282
|
// src/version.ts
|
|
728
|
-
var VERSION = "0.
|
|
1283
|
+
var VERSION = "0.5.0";
|
|
729
1284
|
|
|
730
1285
|
// src/errors/queue.error.ts
|
|
731
1286
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -742,20 +1297,60 @@ var AhkoQueueError = class extends AhkoError {
|
|
|
742
1297
|
}
|
|
743
1298
|
};
|
|
744
1299
|
|
|
745
|
-
// src/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1300
|
+
// src/scheduler/signal.ts
|
|
1301
|
+
function combineSignals(signals) {
|
|
1302
|
+
const activeSignals = signals.filter(
|
|
1303
|
+
(signal) => signal !== void 0
|
|
1304
|
+
);
|
|
1305
|
+
if (activeSignals.length === 0) {
|
|
1306
|
+
const controller2 = new AbortController();
|
|
1307
|
+
return {
|
|
1308
|
+
signal: controller2.signal,
|
|
1309
|
+
cleanup: () => {
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
757
1312
|
}
|
|
758
|
-
|
|
1313
|
+
const alreadyAborted = activeSignals.find((s) => s.aborted);
|
|
1314
|
+
if (alreadyAborted) {
|
|
1315
|
+
const controller2 = new AbortController();
|
|
1316
|
+
controller2.abort(alreadyAborted.reason);
|
|
1317
|
+
return {
|
|
1318
|
+
signal: controller2.signal,
|
|
1319
|
+
cleanup: () => {
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
if (activeSignals.length === 1) {
|
|
1324
|
+
return {
|
|
1325
|
+
signal: activeSignals[0],
|
|
1326
|
+
cleanup: () => {
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const controller = new AbortController();
|
|
1331
|
+
const cleanupFns = [];
|
|
1332
|
+
const onAbort = (event) => {
|
|
1333
|
+
const target = event.target;
|
|
1334
|
+
cleanup();
|
|
1335
|
+
controller.abort(target.reason);
|
|
1336
|
+
};
|
|
1337
|
+
for (const sig of activeSignals) {
|
|
1338
|
+
sig.addEventListener("abort", onAbort, { once: true });
|
|
1339
|
+
cleanupFns.push(() => {
|
|
1340
|
+
sig.removeEventListener("abort", onAbort);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const cleanup = () => {
|
|
1344
|
+
for (const fn of cleanupFns) {
|
|
1345
|
+
fn();
|
|
1346
|
+
}
|
|
1347
|
+
cleanupFns.length = 0;
|
|
1348
|
+
};
|
|
1349
|
+
return {
|
|
1350
|
+
signal: controller.signal,
|
|
1351
|
+
cleanup
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
759
1354
|
// Annotate the CommonJS export names for ESM import in node:
|
|
760
1355
|
0 && (module.exports = {
|
|
761
1356
|
Ahko,
|
|
@@ -769,6 +1364,7 @@ var AhkoTimeoutError = class extends AhkoError {
|
|
|
769
1364
|
EScheduleStrategy,
|
|
770
1365
|
ETaskState,
|
|
771
1366
|
VERSION,
|
|
772
|
-
calculateBackoff
|
|
1367
|
+
calculateBackoff,
|
|
1368
|
+
combineSignals
|
|
773
1369
|
});
|
|
774
1370
|
//# sourceMappingURL=index.cjs.map
|