@mrjacket/ahko 0.4.0 → 0.6.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 +36 -0
- package/README.md +141 -7
- package/dist/ahko.d.ts +87 -7
- package/dist/events/event-emitter.d.ts +34 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/index.cjs +762 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +762 -30
- package/dist/index.js.map +1 -1
- package/dist/models/events.model.d.ts +50 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/options.model.d.ts +17 -0
- package/dist/models/stats.model.d.ts +4 -0
- package/dist/models/strategy.model.d.ts +6 -2
- package/dist/scheduler/debounce-coordinator.d.ts +41 -0
- package/dist/scheduler/task-queue.d.ts +46 -3
- package/dist/scheduler/task-runner.d.ts +2 -0
- package/dist/scheduler/throttle-coordinator.d.ts +43 -0
- package/dist/version.d.ts +1 -1
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -71,6 +71,8 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
71
71
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
72
72
|
EScheduleStrategy2["DELAY"] = "delay";
|
|
73
73
|
EScheduleStrategy2["IDLE"] = "idle";
|
|
74
|
+
EScheduleStrategy2["THROTTLE"] = "throttle";
|
|
75
|
+
EScheduleStrategy2["DEBOUNCE"] = "debounce";
|
|
74
76
|
return EScheduleStrategy2;
|
|
75
77
|
})(EScheduleStrategy || {});
|
|
76
78
|
|
|
@@ -130,6 +132,163 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
130
132
|
return Math.floor(cappedDelay);
|
|
131
133
|
}
|
|
132
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 entry of this.entries.values()) {
|
|
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
|
+
|
|
133
292
|
// src/scheduler/idle-scheduler.ts
|
|
134
293
|
var IdleScheduler = class {
|
|
135
294
|
/**
|
|
@@ -169,10 +328,218 @@ var IdleScheduler = class {
|
|
|
169
328
|
}
|
|
170
329
|
};
|
|
171
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 entry of this.entries.values()) {
|
|
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
|
+
|
|
465
|
+
// src/events/event-emitter.ts
|
|
466
|
+
var AhkoEventEmitter = class {
|
|
467
|
+
listeners = /* @__PURE__ */ new Map();
|
|
468
|
+
/**
|
|
469
|
+
* Subscribes a listener to a specific Ahko lifecycle event.
|
|
470
|
+
*
|
|
471
|
+
* @param event - The event name to subscribe to.
|
|
472
|
+
* @param handler - The callback function to invoke when the event is emitted.
|
|
473
|
+
* @returns An unsubscribe function to remove the listener.
|
|
474
|
+
*/
|
|
475
|
+
on(event, handler) {
|
|
476
|
+
let set = this.listeners.get(event);
|
|
477
|
+
if (!set) {
|
|
478
|
+
set = /* @__PURE__ */ new Set();
|
|
479
|
+
this.listeners.set(event, set);
|
|
480
|
+
}
|
|
481
|
+
set.add(handler);
|
|
482
|
+
return () => {
|
|
483
|
+
this.off(event, handler);
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Unsubscribes a listener from a specific Ahko lifecycle event.
|
|
488
|
+
*
|
|
489
|
+
* @param event - The event name.
|
|
490
|
+
* @param handler - The callback function to remove.
|
|
491
|
+
*/
|
|
492
|
+
off(event, handler) {
|
|
493
|
+
const set = this.listeners.get(event);
|
|
494
|
+
if (set) {
|
|
495
|
+
set.delete(handler);
|
|
496
|
+
if (set.size === 0) {
|
|
497
|
+
this.listeners.delete(event);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Emits an event with the corresponding typed payload to all subscribed listeners.
|
|
503
|
+
* Listener invocations are safely isolated in try/catch to protect scheduler integrity.
|
|
504
|
+
*
|
|
505
|
+
* @param event - The event name to emit.
|
|
506
|
+
* @param payload - The event-specific payload data.
|
|
507
|
+
*/
|
|
508
|
+
emit(event, payload) {
|
|
509
|
+
const set = this.listeners.get(event);
|
|
510
|
+
if (!set || set.size === 0) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const handlers = Array.from(set);
|
|
514
|
+
for (const handler of handlers) {
|
|
515
|
+
try {
|
|
516
|
+
const result = handler(payload);
|
|
517
|
+
if (result && typeof result.catch === "function") {
|
|
518
|
+
result.catch(() => {
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Removes all registered event listeners.
|
|
527
|
+
*/
|
|
528
|
+
clear() {
|
|
529
|
+
this.listeners.clear();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
172
533
|
// src/scheduler/task-queue.ts
|
|
173
534
|
var TaskQueue = class {
|
|
174
535
|
/** Maximum concurrent active tasks */
|
|
175
536
|
concurrency;
|
|
537
|
+
/** Minimum interval in milliseconds between consecutive task starts */
|
|
538
|
+
minIntervalMs;
|
|
539
|
+
/** Timestamp of the most recent task start */
|
|
540
|
+
lastTaskStartTime = 0;
|
|
541
|
+
/** Active rate limit timer for pacing consecutive tasks */
|
|
542
|
+
rateLimitTimer;
|
|
176
543
|
/** Queue of pending task runners waiting for a concurrency slot */
|
|
177
544
|
queue = [];
|
|
178
545
|
/** Set of task runners currently executing */
|
|
@@ -183,6 +550,14 @@ var TaskQueue = class {
|
|
|
183
550
|
idleEntries = /* @__PURE__ */ new Set();
|
|
184
551
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
185
552
|
retryEntries = /* @__PURE__ */ new Set();
|
|
553
|
+
/** Coordinator for debounced tasks with key coalescing */
|
|
554
|
+
debounceCoordinator = new DebounceCoordinator();
|
|
555
|
+
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
556
|
+
throttleCoordinator = new ThrottleCoordinator();
|
|
557
|
+
/** Lifecycle event emitter for task and scheduler events */
|
|
558
|
+
emitter = new AhkoEventEmitter();
|
|
559
|
+
/** Set of pending resolvers awaiting scheduler idle transition */
|
|
560
|
+
idleResolvers = /* @__PURE__ */ new Set();
|
|
186
561
|
/** WeakMap associating task runners with their scheduling options */
|
|
187
562
|
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
188
563
|
/** Cumulative completed tasks counter */
|
|
@@ -193,19 +568,30 @@ var TaskQueue = class {
|
|
|
193
568
|
cancelledTasks = 0;
|
|
194
569
|
/** Cumulative timed out tasks counter */
|
|
195
570
|
timedOutTasks = 0;
|
|
571
|
+
/** Cumulative count of retry attempts triggered */
|
|
572
|
+
retriedTasks = 0;
|
|
573
|
+
/** Cumulative count of tasks dispatched to concurrency slots */
|
|
574
|
+
totalDispatched = 0;
|
|
196
575
|
/**
|
|
197
576
|
* Creates a new TaskQueue.
|
|
198
577
|
*
|
|
199
578
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
200
|
-
* @
|
|
579
|
+
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
580
|
+
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
201
581
|
*/
|
|
202
|
-
constructor(concurrency = Infinity) {
|
|
582
|
+
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
203
583
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
204
584
|
throw new AhkoConfigurationError(
|
|
205
585
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
206
586
|
);
|
|
207
587
|
}
|
|
588
|
+
if (typeof minIntervalMs !== "number" || Number.isNaN(minIntervalMs) || !Number.isFinite(minIntervalMs) || minIntervalMs < 0) {
|
|
589
|
+
throw new AhkoConfigurationError(
|
|
590
|
+
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
208
593
|
this.concurrency = concurrency;
|
|
594
|
+
this.minIntervalMs = minIntervalMs;
|
|
209
595
|
}
|
|
210
596
|
/**
|
|
211
597
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -218,11 +604,24 @@ var TaskQueue = class {
|
|
|
218
604
|
*/
|
|
219
605
|
enqueue(runner, options) {
|
|
220
606
|
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
221
|
-
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */) {
|
|
607
|
+
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */ && strategy !== "throttle" /* THROTTLE */ && strategy !== "debounce" /* DEBOUNCE */) {
|
|
222
608
|
throw new AhkoConfigurationError(
|
|
223
|
-
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
609
|
+
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle", "throttle", "debounce".`
|
|
224
610
|
);
|
|
225
611
|
}
|
|
612
|
+
if (strategy === "throttle" /* THROTTLE */ || strategy === "debounce" /* DEBOUNCE */) {
|
|
613
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
614
|
+
throw new AhkoConfigurationError(
|
|
615
|
+
`Strategy "${strategy}" requires a valid "key" of type string or symbol.`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
619
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
620
|
+
throw new AhkoConfigurationError(
|
|
621
|
+
`Strategy "${strategy}" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
226
625
|
if (options?.retry) {
|
|
227
626
|
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
228
627
|
throw new AhkoConfigurationError(
|
|
@@ -278,6 +677,11 @@ var TaskQueue = class {
|
|
|
278
677
|
if (index !== -1) {
|
|
279
678
|
this.queue.splice(index, 1);
|
|
280
679
|
this.cancelledTasks++;
|
|
680
|
+
this.emitter.emit("task:cancel", {
|
|
681
|
+
taskId: runner.taskId,
|
|
682
|
+
reason: "Task cancelled while queued"
|
|
683
|
+
});
|
|
684
|
+
this.checkIdle();
|
|
281
685
|
}
|
|
282
686
|
};
|
|
283
687
|
this.queue.push(runner);
|
|
@@ -301,6 +705,11 @@ var TaskQueue = class {
|
|
|
301
705
|
if (index !== -1) {
|
|
302
706
|
this.queue.splice(index, 1);
|
|
303
707
|
this.cancelledTasks++;
|
|
708
|
+
this.emitter.emit("task:cancel", {
|
|
709
|
+
taskId: runner.taskId,
|
|
710
|
+
reason: "Task cancelled while queued"
|
|
711
|
+
});
|
|
712
|
+
this.checkIdle();
|
|
304
713
|
}
|
|
305
714
|
};
|
|
306
715
|
this.queue.push(runner);
|
|
@@ -313,6 +722,11 @@ var TaskQueue = class {
|
|
|
313
722
|
clearTimeout(delayedEntry.timerId);
|
|
314
723
|
this.delayedEntries.delete(delayedEntry);
|
|
315
724
|
this.cancelledTasks++;
|
|
725
|
+
this.emitter.emit("task:cancel", {
|
|
726
|
+
taskId: runner.taskId,
|
|
727
|
+
reason: "Task cancelled while waiting in delay"
|
|
728
|
+
});
|
|
729
|
+
this.checkIdle();
|
|
316
730
|
}
|
|
317
731
|
};
|
|
318
732
|
}
|
|
@@ -332,6 +746,11 @@ var TaskQueue = class {
|
|
|
332
746
|
if (index !== -1) {
|
|
333
747
|
this.queue.splice(index, 1);
|
|
334
748
|
this.cancelledTasks++;
|
|
749
|
+
this.emitter.emit("task:cancel", {
|
|
750
|
+
taskId: runner.taskId,
|
|
751
|
+
reason: "Task cancelled while queued"
|
|
752
|
+
});
|
|
753
|
+
this.checkIdle();
|
|
335
754
|
}
|
|
336
755
|
};
|
|
337
756
|
this.queue.push(runner);
|
|
@@ -344,15 +763,51 @@ var TaskQueue = class {
|
|
|
344
763
|
handle.cancel();
|
|
345
764
|
this.idleEntries.delete(idleEntry);
|
|
346
765
|
this.cancelledTasks++;
|
|
766
|
+
this.emitter.emit("task:cancel", {
|
|
767
|
+
taskId: runner.taskId,
|
|
768
|
+
reason: "Task cancelled while waiting for idle"
|
|
769
|
+
});
|
|
770
|
+
this.checkIdle();
|
|
347
771
|
}
|
|
348
772
|
};
|
|
349
773
|
}
|
|
350
774
|
/**
|
|
351
775
|
* Pumps the queue by picking pending tasks and executing them
|
|
352
|
-
* as long as concurrency capacity is available.
|
|
776
|
+
* as long as concurrency capacity is available and minIntervalMs is respected.
|
|
353
777
|
*/
|
|
354
778
|
pump() {
|
|
779
|
+
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
783
|
+
const now = Date.now();
|
|
784
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
785
|
+
if (elapsed < this.minIntervalMs) {
|
|
786
|
+
if (this.rateLimitTimer === void 0) {
|
|
787
|
+
const delay = this.minIntervalMs - elapsed;
|
|
788
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
789
|
+
this.rateLimitTimer = void 0;
|
|
790
|
+
this.pump();
|
|
791
|
+
}, delay);
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
355
796
|
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
797
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
798
|
+
const now = Date.now();
|
|
799
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
800
|
+
if (elapsed < this.minIntervalMs) {
|
|
801
|
+
if (this.rateLimitTimer === void 0) {
|
|
802
|
+
const delay = this.minIntervalMs - elapsed;
|
|
803
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
804
|
+
this.rateLimitTimer = void 0;
|
|
805
|
+
this.pump();
|
|
806
|
+
}, delay);
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
356
811
|
const runner = this.queue.shift();
|
|
357
812
|
if (!runner) {
|
|
358
813
|
break;
|
|
@@ -361,7 +816,19 @@ var TaskQueue = class {
|
|
|
361
816
|
continue;
|
|
362
817
|
}
|
|
363
818
|
this.activeRunners.add(runner);
|
|
819
|
+
this.lastTaskStartTime = Date.now();
|
|
364
820
|
void this.executeRunner(runner);
|
|
821
|
+
if (this.minIntervalMs > 0) {
|
|
822
|
+
if (this.queue.length > 0 && this.activeRunners.size < this.concurrency) {
|
|
823
|
+
if (this.rateLimitTimer === void 0) {
|
|
824
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
825
|
+
this.rateLimitTimer = void 0;
|
|
826
|
+
this.pump();
|
|
827
|
+
}, this.minIntervalMs);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
365
832
|
}
|
|
366
833
|
}
|
|
367
834
|
/**
|
|
@@ -369,36 +836,69 @@ var TaskQueue = class {
|
|
|
369
836
|
*/
|
|
370
837
|
async executeRunner(runner) {
|
|
371
838
|
const options = this.runnerOptions.get(runner);
|
|
839
|
+
this.totalDispatched++;
|
|
840
|
+
this.emitter.emit("task:start", {
|
|
841
|
+
taskId: runner.taskId,
|
|
842
|
+
attempt: runner.attempt
|
|
843
|
+
});
|
|
372
844
|
try {
|
|
373
845
|
const result = await runner.run();
|
|
374
846
|
this.completedTasks++;
|
|
375
847
|
this.activeRunners.delete(runner);
|
|
376
848
|
this.runnerOptions.delete(runner);
|
|
849
|
+
this.emitter.emit("task:complete", {
|
|
850
|
+
taskId: runner.taskId,
|
|
851
|
+
attempt: runner.attempt,
|
|
852
|
+
durationMs: runner.lastDurationMs,
|
|
853
|
+
result
|
|
854
|
+
});
|
|
377
855
|
runner.resolve(result);
|
|
378
856
|
} catch (error) {
|
|
379
857
|
if (runner.state === "cancelled" /* CANCELLED */) {
|
|
380
858
|
this.cancelledTasks++;
|
|
381
859
|
this.activeRunners.delete(runner);
|
|
382
860
|
this.runnerOptions.delete(runner);
|
|
861
|
+
this.emitter.emit("task:cancel", {
|
|
862
|
+
taskId: runner.taskId,
|
|
863
|
+
reason: error
|
|
864
|
+
});
|
|
383
865
|
runner.reject(error);
|
|
384
866
|
return;
|
|
385
867
|
}
|
|
386
868
|
const shouldRetry = await runner.canRetry(error, options?.retry);
|
|
387
869
|
if (shouldRetry) {
|
|
870
|
+
this.retriedTasks++;
|
|
388
871
|
this.activeRunners.delete(runner);
|
|
872
|
+
this.emitter.emit("task:fail", {
|
|
873
|
+
taskId: runner.taskId,
|
|
874
|
+
attempt: runner.attempt - 1,
|
|
875
|
+
error,
|
|
876
|
+
willRetry: true
|
|
877
|
+
});
|
|
389
878
|
this.scheduleRetry(runner, options);
|
|
390
879
|
return;
|
|
391
880
|
}
|
|
392
881
|
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
393
882
|
this.timedOutTasks++;
|
|
883
|
+
this.emitter.emit("task:timeout", {
|
|
884
|
+
taskId: runner.taskId,
|
|
885
|
+
timeoutMs: runner.timeoutMs
|
|
886
|
+
});
|
|
394
887
|
} else {
|
|
395
888
|
this.failedTasks++;
|
|
396
889
|
}
|
|
397
890
|
this.activeRunners.delete(runner);
|
|
398
891
|
this.runnerOptions.delete(runner);
|
|
892
|
+
this.emitter.emit("task:fail", {
|
|
893
|
+
taskId: runner.taskId,
|
|
894
|
+
attempt: runner.attempt,
|
|
895
|
+
error,
|
|
896
|
+
willRetry: false
|
|
897
|
+
});
|
|
399
898
|
runner.reject(error);
|
|
400
899
|
} finally {
|
|
401
900
|
this.pump();
|
|
901
|
+
this.checkIdle();
|
|
402
902
|
}
|
|
403
903
|
}
|
|
404
904
|
/**
|
|
@@ -413,6 +913,11 @@ var TaskQueue = class {
|
|
|
413
913
|
if (index !== -1) {
|
|
414
914
|
this.queue.splice(index, 1);
|
|
415
915
|
this.cancelledTasks++;
|
|
916
|
+
this.emitter.emit("task:cancel", {
|
|
917
|
+
taskId: runner.taskId,
|
|
918
|
+
reason: "Task cancelled while queued"
|
|
919
|
+
});
|
|
920
|
+
this.checkIdle();
|
|
416
921
|
}
|
|
417
922
|
};
|
|
418
923
|
this.queue.push(runner);
|
|
@@ -431,6 +936,11 @@ var TaskQueue = class {
|
|
|
431
936
|
if (index !== -1) {
|
|
432
937
|
this.queue.splice(index, 1);
|
|
433
938
|
this.cancelledTasks++;
|
|
939
|
+
this.emitter.emit("task:cancel", {
|
|
940
|
+
taskId: runner.taskId,
|
|
941
|
+
reason: "Task cancelled while queued"
|
|
942
|
+
});
|
|
943
|
+
this.checkIdle();
|
|
434
944
|
}
|
|
435
945
|
};
|
|
436
946
|
this.queue.push(runner);
|
|
@@ -443,9 +953,91 @@ var TaskQueue = class {
|
|
|
443
953
|
clearTimeout(retryEntry.timerId);
|
|
444
954
|
this.retryEntries.delete(retryEntry);
|
|
445
955
|
this.cancelledTasks++;
|
|
956
|
+
this.emitter.emit("task:cancel", {
|
|
957
|
+
taskId: runner.taskId,
|
|
958
|
+
reason: "Task cancelled during retry backoff"
|
|
959
|
+
});
|
|
960
|
+
this.checkIdle();
|
|
446
961
|
}
|
|
447
962
|
};
|
|
448
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Checks whether the scheduler has transitioned to idle and notifies listeners/resolvers.
|
|
966
|
+
*/
|
|
967
|
+
checkIdle() {
|
|
968
|
+
if (this.isIdle()) {
|
|
969
|
+
if (this.idleResolvers.size > 0) {
|
|
970
|
+
for (const resolve of this.idleResolvers) {
|
|
971
|
+
resolve();
|
|
972
|
+
}
|
|
973
|
+
this.idleResolvers.clear();
|
|
974
|
+
}
|
|
975
|
+
this.emitter.emit("idle", { timestamp: Date.now() });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Checks whether the scheduler is currently idle (no active runners and no pending tasks).
|
|
980
|
+
*
|
|
981
|
+
* @returns True if completely idle, false otherwise.
|
|
982
|
+
*/
|
|
983
|
+
isIdle() {
|
|
984
|
+
return this.activeRunners.size === 0 && this.queue.length === 0 && this.delayedEntries.size === 0 && this.idleEntries.size === 0 && this.retryEntries.size === 0 && this.debounceCoordinator.size === 0 && this.throttleCoordinator.size === 0;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Returns a promise that resolves once the scheduler has processed all tasks and is idle.
|
|
988
|
+
*
|
|
989
|
+
* @returns Promise resolving when idle.
|
|
990
|
+
*/
|
|
991
|
+
onIdle() {
|
|
992
|
+
if (this.isIdle()) {
|
|
993
|
+
return Promise.resolve();
|
|
994
|
+
}
|
|
995
|
+
return new Promise((resolve) => {
|
|
996
|
+
this.idleResolvers.add(resolve);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Clears all pending and waiting tasks from the scheduler, cancelling their runners.
|
|
1001
|
+
* Active tasks currently in flight will continue to run to completion or abort via signal.
|
|
1002
|
+
*/
|
|
1003
|
+
clear() {
|
|
1004
|
+
while (this.queue.length > 0) {
|
|
1005
|
+
const runner = this.queue.shift();
|
|
1006
|
+
if (runner && runner.state !== "cancelled" /* CANCELLED */) {
|
|
1007
|
+
runner.cancel("Scheduler cleared");
|
|
1008
|
+
this.cancelledTasks++;
|
|
1009
|
+
this.emitter.emit("task:cancel", { taskId: runner.taskId, reason: "Scheduler cleared" });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
for (const entry of this.delayedEntries.values()) {
|
|
1013
|
+
clearTimeout(entry.timerId);
|
|
1014
|
+
entry.runner.cancel("Scheduler cleared");
|
|
1015
|
+
this.cancelledTasks++;
|
|
1016
|
+
this.emitter.emit("task:cancel", { taskId: entry.runner.taskId, reason: "Scheduler cleared" });
|
|
1017
|
+
}
|
|
1018
|
+
this.delayedEntries.clear();
|
|
1019
|
+
for (const entry of this.idleEntries.values()) {
|
|
1020
|
+
entry.handle.cancel();
|
|
1021
|
+
entry.runner.cancel("Scheduler cleared");
|
|
1022
|
+
this.cancelledTasks++;
|
|
1023
|
+
this.emitter.emit("task:cancel", { taskId: entry.runner.taskId, reason: "Scheduler cleared" });
|
|
1024
|
+
}
|
|
1025
|
+
this.idleEntries.clear();
|
|
1026
|
+
for (const entry of this.retryEntries.values()) {
|
|
1027
|
+
clearTimeout(entry.timerId);
|
|
1028
|
+
entry.runner.cancel("Scheduler cleared");
|
|
1029
|
+
this.cancelledTasks++;
|
|
1030
|
+
this.emitter.emit("task:cancel", { taskId: entry.runner.taskId, reason: "Scheduler cleared" });
|
|
1031
|
+
}
|
|
1032
|
+
this.retryEntries.clear();
|
|
1033
|
+
this.debounceCoordinator.clear();
|
|
1034
|
+
this.throttleCoordinator.clear();
|
|
1035
|
+
if (this.rateLimitTimer !== void 0) {
|
|
1036
|
+
clearTimeout(this.rateLimitTimer);
|
|
1037
|
+
this.rateLimitTimer = void 0;
|
|
1038
|
+
}
|
|
1039
|
+
this.checkIdle();
|
|
1040
|
+
}
|
|
449
1041
|
/**
|
|
450
1042
|
* Returns telemetry snapshot for the scheduler.
|
|
451
1043
|
*
|
|
@@ -454,31 +1046,18 @@ var TaskQueue = class {
|
|
|
454
1046
|
getStats() {
|
|
455
1047
|
return Object.freeze({
|
|
456
1048
|
activeTasks: this.activeRunners.size,
|
|
457
|
-
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size,
|
|
1049
|
+
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size + this.debounceCoordinator.size + this.throttleCoordinator.size,
|
|
458
1050
|
completedTasks: this.completedTasks,
|
|
459
1051
|
failedTasks: this.failedTasks,
|
|
460
1052
|
cancelledTasks: this.cancelledTasks,
|
|
461
1053
|
timedOutTasks: this.timedOutTasks,
|
|
1054
|
+
retriedTasks: this.retriedTasks,
|
|
1055
|
+
totalDispatched: this.totalDispatched,
|
|
462
1056
|
capacity: this.concurrency
|
|
463
1057
|
});
|
|
464
1058
|
}
|
|
465
1059
|
};
|
|
466
1060
|
|
|
467
|
-
// src/errors/cancellation.error.ts
|
|
468
|
-
var AhkoCancellationError = class extends AhkoError {
|
|
469
|
-
/**
|
|
470
|
-
* Creates a new AhkoCancellationError.
|
|
471
|
-
*
|
|
472
|
-
* @param message - Reason for cancellation.
|
|
473
|
-
* @param options - Standard Error options including cause.
|
|
474
|
-
*/
|
|
475
|
-
constructor(message = "Task was cancelled", options) {
|
|
476
|
-
super(message, options);
|
|
477
|
-
this.name = "AhkoCancellationError";
|
|
478
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
|
|
482
1061
|
// src/scheduler/task-runner.ts
|
|
483
1062
|
var taskIdCounter = 0;
|
|
484
1063
|
var TaskRunner = class {
|
|
@@ -508,6 +1087,8 @@ var TaskRunner = class {
|
|
|
508
1087
|
onCancel;
|
|
509
1088
|
/** Current execution attempt count (1-indexed) */
|
|
510
1089
|
attempt = 1;
|
|
1090
|
+
/** Duration of the most recent execution attempt in milliseconds */
|
|
1091
|
+
lastDurationMs = 0;
|
|
511
1092
|
/**
|
|
512
1093
|
* Creates a new TaskRunner instance.
|
|
513
1094
|
*
|
|
@@ -666,9 +1247,11 @@ var TaskRunner = class {
|
|
|
666
1247
|
if (timeoutPromise) {
|
|
667
1248
|
racePromises.push(timeoutPromise);
|
|
668
1249
|
}
|
|
1250
|
+
const startTime = Date.now();
|
|
669
1251
|
try {
|
|
670
1252
|
const result = await Promise.race(racePromises);
|
|
671
1253
|
this.clearTimeoutTimer();
|
|
1254
|
+
this.lastDurationMs = Math.max(0, Date.now() - startTime);
|
|
672
1255
|
if (abortListener) {
|
|
673
1256
|
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
674
1257
|
}
|
|
@@ -685,6 +1268,7 @@ var TaskRunner = class {
|
|
|
685
1268
|
return result;
|
|
686
1269
|
} catch (error) {
|
|
687
1270
|
this.clearTimeoutTimer();
|
|
1271
|
+
this.lastDurationMs = Math.max(0, Date.now() - startTime);
|
|
688
1272
|
if (abortListener) {
|
|
689
1273
|
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
690
1274
|
}
|
|
@@ -780,7 +1364,7 @@ var Ahko = class {
|
|
|
780
1364
|
* ```
|
|
781
1365
|
*/
|
|
782
1366
|
constructor(options) {
|
|
783
|
-
this.queue = new TaskQueue(options?.concurrency);
|
|
1367
|
+
this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
|
|
784
1368
|
}
|
|
785
1369
|
/**
|
|
786
1370
|
* Schedules a task for execution with full return type inference.
|
|
@@ -792,6 +1376,7 @@ var Ahko = class {
|
|
|
792
1376
|
*
|
|
793
1377
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
794
1378
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1379
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
795
1380
|
*
|
|
796
1381
|
* @example
|
|
797
1382
|
* ```typescript
|
|
@@ -809,6 +1394,47 @@ var Ahko = class {
|
|
|
809
1394
|
if (typeof task !== "function") {
|
|
810
1395
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
811
1396
|
}
|
|
1397
|
+
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1398
|
+
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1399
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1400
|
+
throw new AhkoConfigurationError(
|
|
1401
|
+
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1405
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1406
|
+
throw new AhkoConfigurationError(
|
|
1407
|
+
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return this.queue.debounceCoordinator.schedule(
|
|
1411
|
+
options.key,
|
|
1412
|
+
task,
|
|
1413
|
+
waitMs,
|
|
1414
|
+
options,
|
|
1415
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
if (strategy === "throttle" /* THROTTLE */) {
|
|
1419
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1420
|
+
throw new AhkoConfigurationError(
|
|
1421
|
+
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1425
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1426
|
+
throw new AhkoConfigurationError(
|
|
1427
|
+
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
return this.queue.throttleCoordinator.schedule(
|
|
1431
|
+
options.key,
|
|
1432
|
+
task,
|
|
1433
|
+
waitMs,
|
|
1434
|
+
options,
|
|
1435
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
812
1438
|
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
813
1439
|
return this.queue.enqueue(runner, options);
|
|
814
1440
|
}
|
|
@@ -827,13 +1453,6 @@ var Ahko = class {
|
|
|
827
1453
|
*
|
|
828
1454
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
829
1455
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
830
|
-
*
|
|
831
|
-
* @example
|
|
832
|
-
* ```typescript
|
|
833
|
-
* const result = await ahko.idle(async ({ signal }) => {
|
|
834
|
-
* return computeAnalytics();
|
|
835
|
-
* });
|
|
836
|
-
* ```
|
|
837
1456
|
*/
|
|
838
1457
|
idle(task, options) {
|
|
839
1458
|
return this.schedule(task, {
|
|
@@ -841,6 +1460,42 @@ var Ahko = class {
|
|
|
841
1460
|
strategy: "idle" /* IDLE */
|
|
842
1461
|
});
|
|
843
1462
|
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Convenience method to schedule a debounced task with key-based Promise coalescing.
|
|
1465
|
+
*
|
|
1466
|
+
* @template T - Inferred return type of the task.
|
|
1467
|
+
* @param key - Explicit identity key.
|
|
1468
|
+
* @param task - Work to execute once calls stop arriving.
|
|
1469
|
+
* @param waitMs - Quiet window duration in milliseconds.
|
|
1470
|
+
* @param options - Additional schedule options.
|
|
1471
|
+
* @returns Shared promise resolving with the final execution outcome.
|
|
1472
|
+
*/
|
|
1473
|
+
debounce(key, task, waitMs, options) {
|
|
1474
|
+
return this.schedule(task, {
|
|
1475
|
+
...options,
|
|
1476
|
+
strategy: "debounce" /* DEBOUNCE */,
|
|
1477
|
+
key,
|
|
1478
|
+
waitMs
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Convenience method to schedule a throttled task with leading execution and coalesced trailing run.
|
|
1483
|
+
*
|
|
1484
|
+
* @template T - Inferred return type of the task.
|
|
1485
|
+
* @param key - Explicit identity key.
|
|
1486
|
+
* @param task - Work to execute.
|
|
1487
|
+
* @param waitMs - Throttle interval duration in milliseconds.
|
|
1488
|
+
* @param options - Additional schedule options.
|
|
1489
|
+
* @returns Promise resolving with the leading or coalesced trailing result.
|
|
1490
|
+
*/
|
|
1491
|
+
throttle(key, task, waitMs, options) {
|
|
1492
|
+
return this.schedule(task, {
|
|
1493
|
+
...options,
|
|
1494
|
+
strategy: "throttle" /* THROTTLE */,
|
|
1495
|
+
key,
|
|
1496
|
+
waitMs
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
844
1499
|
/**
|
|
845
1500
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
846
1501
|
*
|
|
@@ -855,10 +1510,87 @@ var Ahko = class {
|
|
|
855
1510
|
stats() {
|
|
856
1511
|
return this.queue.getStats();
|
|
857
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Subscribes to a scheduler lifecycle event.
|
|
1515
|
+
*
|
|
1516
|
+
* @param event - Event name to listen for.
|
|
1517
|
+
* @param handler - Callback function invoked when the event is emitted.
|
|
1518
|
+
* @returns Unsubscribe function to remove the listener.
|
|
1519
|
+
*
|
|
1520
|
+
* @example
|
|
1521
|
+
* ```typescript
|
|
1522
|
+
* const unsubscribe = ahko.on("task:start", ({ taskId, attempt }) => {
|
|
1523
|
+
* console.log(`Task ${taskId} started attempt ${attempt}`);
|
|
1524
|
+
* });
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1527
|
+
on(event, handler) {
|
|
1528
|
+
return this.queue.emitter.on(event, handler);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Unsubscribes an event listener from a scheduler lifecycle event.
|
|
1532
|
+
*
|
|
1533
|
+
* @param event - Event name.
|
|
1534
|
+
* @param handler - The exact listener callback to remove.
|
|
1535
|
+
*/
|
|
1536
|
+
off(event, handler) {
|
|
1537
|
+
this.queue.emitter.off(event, handler);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Checks whether the scheduler is currently idle (no active runners and no pending tasks).
|
|
1541
|
+
*
|
|
1542
|
+
* @returns True if completely idle, false otherwise.
|
|
1543
|
+
*/
|
|
1544
|
+
isIdle() {
|
|
1545
|
+
return this.queue.isIdle();
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Returns a promise that resolves once the scheduler has completed all tasks and is idle.
|
|
1549
|
+
*
|
|
1550
|
+
* @returns Promise resolving when the scheduler is idle.
|
|
1551
|
+
*
|
|
1552
|
+
* @example
|
|
1553
|
+
* ```typescript
|
|
1554
|
+
* ahko.schedule(doWork);
|
|
1555
|
+
* await ahko.onIdle();
|
|
1556
|
+
* console.log("All work finished!");
|
|
1557
|
+
* ```
|
|
1558
|
+
*/
|
|
1559
|
+
onIdle() {
|
|
1560
|
+
return this.queue.onIdle();
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Clears all pending, delayed, and throttled/debounced tasks from the scheduler.
|
|
1564
|
+
* In-flight active tasks will continue executing to completion or abort via signal.
|
|
1565
|
+
*/
|
|
1566
|
+
clear() {
|
|
1567
|
+
this.queue.clear();
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Returns the delightful Ahko mascot battery telemetry status.
|
|
1571
|
+
*
|
|
1572
|
+
* Low energy, completely chill.
|
|
1573
|
+
*/
|
|
1574
|
+
battery() {
|
|
1575
|
+
return {
|
|
1576
|
+
level: 3,
|
|
1577
|
+
chill: true,
|
|
1578
|
+
status: "low-energy",
|
|
1579
|
+
quote: "Mwee... my battery is low, but all your tasks are handled completely chill."
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Delightful alias for `onIdle()`: wait for all tasks to settle chill and relaxed.
|
|
1584
|
+
*
|
|
1585
|
+
* @returns Promise resolving when all tasks have finished.
|
|
1586
|
+
*/
|
|
1587
|
+
chill() {
|
|
1588
|
+
return this.onIdle();
|
|
1589
|
+
}
|
|
858
1590
|
};
|
|
859
1591
|
|
|
860
1592
|
// src/version.ts
|
|
861
|
-
var VERSION = "0.
|
|
1593
|
+
var VERSION = "0.6.0";
|
|
862
1594
|
|
|
863
1595
|
// src/errors/queue.error.ts
|
|
864
1596
|
var AhkoQueueError = class extends AhkoError {
|