@mrjacket/ahko 0.4.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 +14 -0
- package/README.md +38 -2
- package/dist/ahko.d.ts +23 -7
- package/dist/index.cjs +452 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +452 -30
- package/dist/index.js.map +1 -1
- package/dist/models/options.model.d.ts +17 -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 +16 -3
- 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
|
@@ -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 [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
|
+
|
|
133
292
|
// src/scheduler/idle-scheduler.ts
|
|
134
293
|
var IdleScheduler = class {
|
|
135
294
|
/**
|
|
@@ -169,10 +328,150 @@ 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 [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
|
+
|
|
172
465
|
// src/scheduler/task-queue.ts
|
|
173
466
|
var TaskQueue = class {
|
|
174
467
|
/** Maximum concurrent active tasks */
|
|
175
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;
|
|
176
475
|
/** Queue of pending task runners waiting for a concurrency slot */
|
|
177
476
|
queue = [];
|
|
178
477
|
/** Set of task runners currently executing */
|
|
@@ -183,6 +482,10 @@ var TaskQueue = class {
|
|
|
183
482
|
idleEntries = /* @__PURE__ */ new Set();
|
|
184
483
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
185
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();
|
|
186
489
|
/** WeakMap associating task runners with their scheduling options */
|
|
187
490
|
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
188
491
|
/** Cumulative completed tasks counter */
|
|
@@ -197,15 +500,22 @@ var TaskQueue = class {
|
|
|
197
500
|
* Creates a new TaskQueue.
|
|
198
501
|
*
|
|
199
502
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
200
|
-
* @
|
|
503
|
+
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
504
|
+
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
201
505
|
*/
|
|
202
|
-
constructor(concurrency = Infinity) {
|
|
506
|
+
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
203
507
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
204
508
|
throw new AhkoConfigurationError(
|
|
205
509
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
206
510
|
);
|
|
207
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
|
+
}
|
|
208
517
|
this.concurrency = concurrency;
|
|
518
|
+
this.minIntervalMs = minIntervalMs;
|
|
209
519
|
}
|
|
210
520
|
/**
|
|
211
521
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -218,11 +528,24 @@ var TaskQueue = class {
|
|
|
218
528
|
*/
|
|
219
529
|
enqueue(runner, options) {
|
|
220
530
|
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
221
|
-
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 */) {
|
|
222
532
|
throw new AhkoConfigurationError(
|
|
223
|
-
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
533
|
+
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle", "throttle", "debounce".`
|
|
224
534
|
);
|
|
225
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
|
+
}
|
|
226
549
|
if (options?.retry) {
|
|
227
550
|
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
228
551
|
throw new AhkoConfigurationError(
|
|
@@ -349,10 +672,41 @@ var TaskQueue = class {
|
|
|
349
672
|
}
|
|
350
673
|
/**
|
|
351
674
|
* Pumps the queue by picking pending tasks and executing them
|
|
352
|
-
* as long as concurrency capacity is available.
|
|
675
|
+
* as long as concurrency capacity is available and minIntervalMs is respected.
|
|
353
676
|
*/
|
|
354
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
|
+
}
|
|
355
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
|
+
}
|
|
356
710
|
const runner = this.queue.shift();
|
|
357
711
|
if (!runner) {
|
|
358
712
|
break;
|
|
@@ -361,7 +715,19 @@ var TaskQueue = class {
|
|
|
361
715
|
continue;
|
|
362
716
|
}
|
|
363
717
|
this.activeRunners.add(runner);
|
|
718
|
+
this.lastTaskStartTime = Date.now();
|
|
364
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
|
+
}
|
|
365
731
|
}
|
|
366
732
|
}
|
|
367
733
|
/**
|
|
@@ -454,7 +820,7 @@ var TaskQueue = class {
|
|
|
454
820
|
getStats() {
|
|
455
821
|
return Object.freeze({
|
|
456
822
|
activeTasks: this.activeRunners.size,
|
|
457
|
-
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,
|
|
458
824
|
completedTasks: this.completedTasks,
|
|
459
825
|
failedTasks: this.failedTasks,
|
|
460
826
|
cancelledTasks: this.cancelledTasks,
|
|
@@ -464,21 +830,6 @@ var TaskQueue = class {
|
|
|
464
830
|
}
|
|
465
831
|
};
|
|
466
832
|
|
|
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
833
|
// src/scheduler/task-runner.ts
|
|
483
834
|
var taskIdCounter = 0;
|
|
484
835
|
var TaskRunner = class {
|
|
@@ -780,7 +1131,7 @@ var Ahko = class {
|
|
|
780
1131
|
* ```
|
|
781
1132
|
*/
|
|
782
1133
|
constructor(options) {
|
|
783
|
-
this.queue = new TaskQueue(options?.concurrency);
|
|
1134
|
+
this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
|
|
784
1135
|
}
|
|
785
1136
|
/**
|
|
786
1137
|
* Schedules a task for execution with full return type inference.
|
|
@@ -792,6 +1143,7 @@ var Ahko = class {
|
|
|
792
1143
|
*
|
|
793
1144
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
794
1145
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1146
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
795
1147
|
*
|
|
796
1148
|
* @example
|
|
797
1149
|
* ```typescript
|
|
@@ -809,6 +1161,47 @@ var Ahko = class {
|
|
|
809
1161
|
if (typeof task !== "function") {
|
|
810
1162
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
811
1163
|
}
|
|
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
|
+
}
|
|
812
1205
|
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
813
1206
|
return this.queue.enqueue(runner, options);
|
|
814
1207
|
}
|
|
@@ -827,13 +1220,6 @@ var Ahko = class {
|
|
|
827
1220
|
*
|
|
828
1221
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
829
1222
|
* @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
1223
|
*/
|
|
838
1224
|
idle(task, options) {
|
|
839
1225
|
return this.schedule(task, {
|
|
@@ -841,6 +1227,42 @@ var Ahko = class {
|
|
|
841
1227
|
strategy: "idle" /* IDLE */
|
|
842
1228
|
});
|
|
843
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
|
+
}
|
|
844
1266
|
/**
|
|
845
1267
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
846
1268
|
*
|
|
@@ -858,7 +1280,7 @@ var Ahko = class {
|
|
|
858
1280
|
};
|
|
859
1281
|
|
|
860
1282
|
// src/version.ts
|
|
861
|
-
var VERSION = "0.
|
|
1283
|
+
var VERSION = "0.5.0";
|
|
862
1284
|
|
|
863
1285
|
// src/errors/queue.error.ts
|
|
864
1286
|
var AhkoQueueError = class extends AhkoError {
|