@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.js
CHANGED
|
@@ -33,6 +33,8 @@ var EScheduleStrategy = /* @__PURE__ */ ((EScheduleStrategy2) => {
|
|
|
33
33
|
EScheduleStrategy2["IMMEDIATE"] = "immediate";
|
|
34
34
|
EScheduleStrategy2["DELAY"] = "delay";
|
|
35
35
|
EScheduleStrategy2["IDLE"] = "idle";
|
|
36
|
+
EScheduleStrategy2["THROTTLE"] = "throttle";
|
|
37
|
+
EScheduleStrategy2["DEBOUNCE"] = "debounce";
|
|
36
38
|
return EScheduleStrategy2;
|
|
37
39
|
})(EScheduleStrategy || {});
|
|
38
40
|
|
|
@@ -92,6 +94,163 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
92
94
|
return Math.floor(cappedDelay);
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// src/errors/cancellation.error.ts
|
|
98
|
+
var AhkoCancellationError = class extends AhkoError {
|
|
99
|
+
/**
|
|
100
|
+
* Creates a new AhkoCancellationError.
|
|
101
|
+
*
|
|
102
|
+
* @param message - Reason for cancellation.
|
|
103
|
+
* @param options - Standard Error options including cause.
|
|
104
|
+
*/
|
|
105
|
+
constructor(message = "Task was cancelled", options) {
|
|
106
|
+
super(message, options);
|
|
107
|
+
this.name = "AhkoCancellationError";
|
|
108
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/scheduler/debounce-coordinator.ts
|
|
113
|
+
var DebounceCoordinator = class {
|
|
114
|
+
entries = /* @__PURE__ */ new Map();
|
|
115
|
+
/**
|
|
116
|
+
* Schedules a task under the debounce strategy.
|
|
117
|
+
*
|
|
118
|
+
* @param key - Explicit identity key.
|
|
119
|
+
* @param task - Work to execute once calls stop arriving.
|
|
120
|
+
* @param waitMs - Quiet window duration in milliseconds.
|
|
121
|
+
* @param options - Scheduling options.
|
|
122
|
+
* @param dispatchFn - Callback invoked when the debounce window expires to dispatch the task to the queue.
|
|
123
|
+
* @returns Shared promise that resolves/rejects with the final execution outcome.
|
|
124
|
+
*/
|
|
125
|
+
schedule(key, task, waitMs, options, dispatchFn) {
|
|
126
|
+
const existing = this.entries.get(key);
|
|
127
|
+
if (existing) {
|
|
128
|
+
clearTimeout(existing.timerId);
|
|
129
|
+
if (existing.options?.signal && existing.abortListener) {
|
|
130
|
+
existing.options.signal.removeEventListener("abort", existing.abortListener);
|
|
131
|
+
}
|
|
132
|
+
existing.task = task;
|
|
133
|
+
existing.options = options;
|
|
134
|
+
if (options?.signal?.aborted) {
|
|
135
|
+
this.entries.delete(key);
|
|
136
|
+
const err = new AhkoCancellationError(
|
|
137
|
+
typeof options.signal.reason === "string" ? options.signal.reason : "Debounced task was cancelled prior to execution",
|
|
138
|
+
{ cause: options.signal.reason instanceof Error ? options.signal.reason : void 0 }
|
|
139
|
+
);
|
|
140
|
+
existing.reject(err);
|
|
141
|
+
return existing.promise;
|
|
142
|
+
}
|
|
143
|
+
if (options?.signal) {
|
|
144
|
+
const listener = () => {
|
|
145
|
+
this.cancel(key, options.signal?.reason);
|
|
146
|
+
};
|
|
147
|
+
existing.abortListener = listener;
|
|
148
|
+
options.signal.addEventListener("abort", listener, { once: true });
|
|
149
|
+
}
|
|
150
|
+
existing.timerId = setTimeout(() => {
|
|
151
|
+
void this.flush(key, dispatchFn);
|
|
152
|
+
}, waitMs);
|
|
153
|
+
return existing.promise;
|
|
154
|
+
}
|
|
155
|
+
let resolvePromise;
|
|
156
|
+
let rejectPromise;
|
|
157
|
+
const promise = new Promise((resolve, reject) => {
|
|
158
|
+
resolvePromise = resolve;
|
|
159
|
+
rejectPromise = reject;
|
|
160
|
+
});
|
|
161
|
+
if (options?.signal?.aborted) {
|
|
162
|
+
const err = new AhkoCancellationError(
|
|
163
|
+
typeof options.signal.reason === "string" ? options.signal.reason : "Debounced task was cancelled prior to execution",
|
|
164
|
+
{ cause: options.signal.reason instanceof Error ? options.signal.reason : void 0 }
|
|
165
|
+
);
|
|
166
|
+
rejectPromise(err);
|
|
167
|
+
return promise;
|
|
168
|
+
}
|
|
169
|
+
let abortListener;
|
|
170
|
+
if (options?.signal) {
|
|
171
|
+
abortListener = () => {
|
|
172
|
+
this.cancel(key, options.signal?.reason);
|
|
173
|
+
};
|
|
174
|
+
options.signal.addEventListener("abort", abortListener, { once: true });
|
|
175
|
+
}
|
|
176
|
+
const timerId = setTimeout(() => {
|
|
177
|
+
void this.flush(key, dispatchFn);
|
|
178
|
+
}, waitMs);
|
|
179
|
+
const entry = {
|
|
180
|
+
key,
|
|
181
|
+
task,
|
|
182
|
+
options,
|
|
183
|
+
timerId,
|
|
184
|
+
resolve: resolvePromise,
|
|
185
|
+
reject: rejectPromise,
|
|
186
|
+
promise,
|
|
187
|
+
abortListener
|
|
188
|
+
};
|
|
189
|
+
this.entries.set(key, entry);
|
|
190
|
+
return promise;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Dispatches the coalesced task when the quiet window expires.
|
|
194
|
+
*/
|
|
195
|
+
async flush(key, dispatchFn) {
|
|
196
|
+
const entry = this.entries.get(key);
|
|
197
|
+
if (!entry) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.entries.delete(key);
|
|
201
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
202
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const result = await dispatchFn(entry.task, entry.options);
|
|
206
|
+
entry.resolve(result);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
entry.reject(error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Cancels a pending debounced task by key.
|
|
213
|
+
*
|
|
214
|
+
* @param key - Identity key to cancel.
|
|
215
|
+
* @param reason - Optional cancellation reason.
|
|
216
|
+
*/
|
|
217
|
+
cancel(key, reason) {
|
|
218
|
+
const entry = this.entries.get(key);
|
|
219
|
+
if (!entry) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
clearTimeout(entry.timerId);
|
|
223
|
+
this.entries.delete(key);
|
|
224
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
225
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
226
|
+
}
|
|
227
|
+
const cancelError = new AhkoCancellationError(
|
|
228
|
+
typeof reason === "string" ? reason : "Debounced task was cancelled prior to execution",
|
|
229
|
+
{ cause: reason instanceof Error ? reason : void 0 }
|
|
230
|
+
);
|
|
231
|
+
entry.reject(cancelError);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Number of pending debounced tasks waiting for quiet window expiry.
|
|
235
|
+
*/
|
|
236
|
+
get size() {
|
|
237
|
+
return this.entries.size;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Cancels all pending debounced entries and clears the map.
|
|
241
|
+
*/
|
|
242
|
+
clear() {
|
|
243
|
+
for (const [key, entry] of this.entries) {
|
|
244
|
+
clearTimeout(entry.timerId);
|
|
245
|
+
if (entry.options?.signal && entry.abortListener) {
|
|
246
|
+
entry.options.signal.removeEventListener("abort", entry.abortListener);
|
|
247
|
+
}
|
|
248
|
+
entry.reject(new AhkoCancellationError("Debounced tasks cleared"));
|
|
249
|
+
}
|
|
250
|
+
this.entries.clear();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
95
254
|
// src/scheduler/idle-scheduler.ts
|
|
96
255
|
var IdleScheduler = class {
|
|
97
256
|
/**
|
|
@@ -131,10 +290,150 @@ var IdleScheduler = class {
|
|
|
131
290
|
}
|
|
132
291
|
};
|
|
133
292
|
|
|
293
|
+
// src/scheduler/throttle-coordinator.ts
|
|
294
|
+
var ThrottleCoordinator = class {
|
|
295
|
+
entries = /* @__PURE__ */ new Map();
|
|
296
|
+
/**
|
|
297
|
+
* Schedules a task under the throttle strategy.
|
|
298
|
+
*
|
|
299
|
+
* @param key - Explicit identity key.
|
|
300
|
+
* @param task - Work to execute.
|
|
301
|
+
* @param waitMs - Throttle interval duration in milliseconds.
|
|
302
|
+
* @param options - Scheduling options.
|
|
303
|
+
* @param dispatchFn - Callback invoked to dispatch task execution into the queue.
|
|
304
|
+
* @returns Promise resolving with the leading execution or coalesced trailing result.
|
|
305
|
+
*/
|
|
306
|
+
schedule(key, task, waitMs, options, dispatchFn) {
|
|
307
|
+
const existing = this.entries.get(key);
|
|
308
|
+
if (!existing) {
|
|
309
|
+
const entry = {
|
|
310
|
+
key
|
|
311
|
+
};
|
|
312
|
+
entry.windowTimerId = setTimeout(() => {
|
|
313
|
+
void this.onWindowExpire(key, waitMs, dispatchFn);
|
|
314
|
+
}, waitMs);
|
|
315
|
+
this.entries.set(key, entry);
|
|
316
|
+
return dispatchFn(task, options);
|
|
317
|
+
}
|
|
318
|
+
existing.trailingTask = task;
|
|
319
|
+
existing.trailingOptions = options;
|
|
320
|
+
if (existing.trailingPromise) {
|
|
321
|
+
return existing.trailingPromise;
|
|
322
|
+
}
|
|
323
|
+
let resolvePromise;
|
|
324
|
+
let rejectPromise;
|
|
325
|
+
existing.trailingPromise = new Promise((resolve, reject) => {
|
|
326
|
+
resolvePromise = resolve;
|
|
327
|
+
rejectPromise = reject;
|
|
328
|
+
});
|
|
329
|
+
existing.trailingResolve = resolvePromise;
|
|
330
|
+
existing.trailingReject = rejectPromise;
|
|
331
|
+
if (options?.signal) {
|
|
332
|
+
const listener = () => {
|
|
333
|
+
if (existing.trailingReject) {
|
|
334
|
+
existing.trailingReject(
|
|
335
|
+
new AhkoCancellationError("Throttled trailing task was cancelled", {
|
|
336
|
+
cause: options.signal?.reason instanceof Error ? options.signal.reason : void 0
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
existing.trailingTask = void 0;
|
|
340
|
+
existing.trailingOptions = void 0;
|
|
341
|
+
existing.trailingPromise = void 0;
|
|
342
|
+
existing.trailingResolve = void 0;
|
|
343
|
+
existing.trailingReject = void 0;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
existing.abortListener = listener;
|
|
347
|
+
options.signal.addEventListener("abort", listener, { once: true });
|
|
348
|
+
}
|
|
349
|
+
return existing.trailingPromise;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Invoked when the throttle interval window timer expires.
|
|
353
|
+
*/
|
|
354
|
+
async onWindowExpire(key, waitMs, dispatchFn) {
|
|
355
|
+
const entry = this.entries.get(key);
|
|
356
|
+
if (!entry) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (entry.trailingTask) {
|
|
360
|
+
const task = entry.trailingTask;
|
|
361
|
+
const options = entry.trailingOptions;
|
|
362
|
+
const resolve = entry.trailingResolve;
|
|
363
|
+
const reject = entry.trailingReject;
|
|
364
|
+
entry.trailingTask = void 0;
|
|
365
|
+
entry.trailingOptions = void 0;
|
|
366
|
+
entry.trailingPromise = void 0;
|
|
367
|
+
entry.trailingResolve = void 0;
|
|
368
|
+
entry.trailingReject = void 0;
|
|
369
|
+
entry.windowTimerId = setTimeout(() => {
|
|
370
|
+
void this.onWindowExpire(key, waitMs, dispatchFn);
|
|
371
|
+
}, waitMs);
|
|
372
|
+
try {
|
|
373
|
+
const result = await dispatchFn(task, options);
|
|
374
|
+
resolve?.(result);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
reject?.(error);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.entries.delete(key);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Cancels any pending trailing throttled task for a given key.
|
|
384
|
+
*
|
|
385
|
+
* @param key - Identity key to cancel.
|
|
386
|
+
* @param reason - Optional cancellation reason.
|
|
387
|
+
*/
|
|
388
|
+
cancel(key, reason) {
|
|
389
|
+
const entry = this.entries.get(key);
|
|
390
|
+
if (!entry) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (entry.windowTimerId !== void 0) {
|
|
394
|
+
clearTimeout(entry.windowTimerId);
|
|
395
|
+
}
|
|
396
|
+
this.entries.delete(key);
|
|
397
|
+
if (entry.trailingReject) {
|
|
398
|
+
const cancelError = new AhkoCancellationError(
|
|
399
|
+
typeof reason === "string" ? reason : "Throttled task was cancelled",
|
|
400
|
+
{ cause: reason instanceof Error ? reason : void 0 }
|
|
401
|
+
);
|
|
402
|
+
entry.trailingReject(cancelError);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Number of keys currently actively throttled.
|
|
407
|
+
*/
|
|
408
|
+
get size() {
|
|
409
|
+
return this.entries.size;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Clears all throttled entries and timers.
|
|
413
|
+
*/
|
|
414
|
+
clear() {
|
|
415
|
+
for (const [key, entry] of this.entries) {
|
|
416
|
+
if (entry.windowTimerId !== void 0) {
|
|
417
|
+
clearTimeout(entry.windowTimerId);
|
|
418
|
+
}
|
|
419
|
+
if (entry.trailingReject) {
|
|
420
|
+
entry.trailingReject(new AhkoCancellationError("Throttled tasks cleared"));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
this.entries.clear();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
134
427
|
// src/scheduler/task-queue.ts
|
|
135
428
|
var TaskQueue = class {
|
|
136
429
|
/** Maximum concurrent active tasks */
|
|
137
430
|
concurrency;
|
|
431
|
+
/** Minimum interval in milliseconds between consecutive task starts */
|
|
432
|
+
minIntervalMs;
|
|
433
|
+
/** Timestamp of the most recent task start */
|
|
434
|
+
lastTaskStartTime = 0;
|
|
435
|
+
/** Active rate limit timer for pacing consecutive tasks */
|
|
436
|
+
rateLimitTimer;
|
|
138
437
|
/** Queue of pending task runners waiting for a concurrency slot */
|
|
139
438
|
queue = [];
|
|
140
439
|
/** Set of task runners currently executing */
|
|
@@ -145,6 +444,10 @@ var TaskQueue = class {
|
|
|
145
444
|
idleEntries = /* @__PURE__ */ new Set();
|
|
146
445
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
147
446
|
retryEntries = /* @__PURE__ */ new Set();
|
|
447
|
+
/** Coordinator for debounced tasks with key coalescing */
|
|
448
|
+
debounceCoordinator = new DebounceCoordinator();
|
|
449
|
+
/** Coordinator for throttled tasks with leading/trailing coalescing */
|
|
450
|
+
throttleCoordinator = new ThrottleCoordinator();
|
|
148
451
|
/** WeakMap associating task runners with their scheduling options */
|
|
149
452
|
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
150
453
|
/** Cumulative completed tasks counter */
|
|
@@ -159,15 +462,22 @@ var TaskQueue = class {
|
|
|
159
462
|
* Creates a new TaskQueue.
|
|
160
463
|
*
|
|
161
464
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
162
|
-
* @
|
|
465
|
+
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
466
|
+
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
163
467
|
*/
|
|
164
|
-
constructor(concurrency = Infinity) {
|
|
468
|
+
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
165
469
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
166
470
|
throw new AhkoConfigurationError(
|
|
167
471
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
168
472
|
);
|
|
169
473
|
}
|
|
474
|
+
if (typeof minIntervalMs !== "number" || Number.isNaN(minIntervalMs) || !Number.isFinite(minIntervalMs) || minIntervalMs < 0) {
|
|
475
|
+
throw new AhkoConfigurationError(
|
|
476
|
+
`Invalid minIntervalMs "${minIntervalMs}". minIntervalMs must be a non-negative finite number.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
170
479
|
this.concurrency = concurrency;
|
|
480
|
+
this.minIntervalMs = minIntervalMs;
|
|
171
481
|
}
|
|
172
482
|
/**
|
|
173
483
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -180,11 +490,24 @@ var TaskQueue = class {
|
|
|
180
490
|
*/
|
|
181
491
|
enqueue(runner, options) {
|
|
182
492
|
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
183
|
-
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */) {
|
|
493
|
+
if (strategy !== "immediate" /* IMMEDIATE */ && strategy !== "delay" /* DELAY */ && strategy !== "idle" /* IDLE */ && strategy !== "throttle" /* THROTTLE */ && strategy !== "debounce" /* DEBOUNCE */) {
|
|
184
494
|
throw new AhkoConfigurationError(
|
|
185
|
-
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
495
|
+
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle", "throttle", "debounce".`
|
|
186
496
|
);
|
|
187
497
|
}
|
|
498
|
+
if (strategy === "throttle" /* THROTTLE */ || strategy === "debounce" /* DEBOUNCE */) {
|
|
499
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
500
|
+
throw new AhkoConfigurationError(
|
|
501
|
+
`Strategy "${strategy}" requires a valid "key" of type string or symbol.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
505
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
506
|
+
throw new AhkoConfigurationError(
|
|
507
|
+
`Strategy "${strategy}" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
188
511
|
if (options?.retry) {
|
|
189
512
|
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
190
513
|
throw new AhkoConfigurationError(
|
|
@@ -311,10 +634,41 @@ var TaskQueue = class {
|
|
|
311
634
|
}
|
|
312
635
|
/**
|
|
313
636
|
* Pumps the queue by picking pending tasks and executing them
|
|
314
|
-
* as long as concurrency capacity is available.
|
|
637
|
+
* as long as concurrency capacity is available and minIntervalMs is respected.
|
|
315
638
|
*/
|
|
316
639
|
pump() {
|
|
640
|
+
if (this.queue.length === 0 || this.activeRunners.size >= this.concurrency) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
646
|
+
if (elapsed < this.minIntervalMs) {
|
|
647
|
+
if (this.rateLimitTimer === void 0) {
|
|
648
|
+
const delay = this.minIntervalMs - elapsed;
|
|
649
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
650
|
+
this.rateLimitTimer = void 0;
|
|
651
|
+
this.pump();
|
|
652
|
+
}, delay);
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
317
657
|
while (this.activeRunners.size < this.concurrency && this.queue.length > 0) {
|
|
658
|
+
if (this.minIntervalMs > 0 && this.lastTaskStartTime > 0) {
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
const elapsed = now - this.lastTaskStartTime;
|
|
661
|
+
if (elapsed < this.minIntervalMs) {
|
|
662
|
+
if (this.rateLimitTimer === void 0) {
|
|
663
|
+
const delay = this.minIntervalMs - elapsed;
|
|
664
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
665
|
+
this.rateLimitTimer = void 0;
|
|
666
|
+
this.pump();
|
|
667
|
+
}, delay);
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
318
672
|
const runner = this.queue.shift();
|
|
319
673
|
if (!runner) {
|
|
320
674
|
break;
|
|
@@ -323,7 +677,19 @@ var TaskQueue = class {
|
|
|
323
677
|
continue;
|
|
324
678
|
}
|
|
325
679
|
this.activeRunners.add(runner);
|
|
680
|
+
this.lastTaskStartTime = Date.now();
|
|
326
681
|
void this.executeRunner(runner);
|
|
682
|
+
if (this.minIntervalMs > 0) {
|
|
683
|
+
if (this.queue.length > 0 && this.activeRunners.size < this.concurrency) {
|
|
684
|
+
if (this.rateLimitTimer === void 0) {
|
|
685
|
+
this.rateLimitTimer = setTimeout(() => {
|
|
686
|
+
this.rateLimitTimer = void 0;
|
|
687
|
+
this.pump();
|
|
688
|
+
}, this.minIntervalMs);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
327
693
|
}
|
|
328
694
|
}
|
|
329
695
|
/**
|
|
@@ -416,7 +782,7 @@ var TaskQueue = class {
|
|
|
416
782
|
getStats() {
|
|
417
783
|
return Object.freeze({
|
|
418
784
|
activeTasks: this.activeRunners.size,
|
|
419
|
-
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size,
|
|
785
|
+
pendingTasks: this.queue.length + this.delayedEntries.size + this.idleEntries.size + this.retryEntries.size + this.debounceCoordinator.size + this.throttleCoordinator.size,
|
|
420
786
|
completedTasks: this.completedTasks,
|
|
421
787
|
failedTasks: this.failedTasks,
|
|
422
788
|
cancelledTasks: this.cancelledTasks,
|
|
@@ -426,21 +792,6 @@ var TaskQueue = class {
|
|
|
426
792
|
}
|
|
427
793
|
};
|
|
428
794
|
|
|
429
|
-
// src/errors/cancellation.error.ts
|
|
430
|
-
var AhkoCancellationError = class extends AhkoError {
|
|
431
|
-
/**
|
|
432
|
-
* Creates a new AhkoCancellationError.
|
|
433
|
-
*
|
|
434
|
-
* @param message - Reason for cancellation.
|
|
435
|
-
* @param options - Standard Error options including cause.
|
|
436
|
-
*/
|
|
437
|
-
constructor(message = "Task was cancelled", options) {
|
|
438
|
-
super(message, options);
|
|
439
|
-
this.name = "AhkoCancellationError";
|
|
440
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
|
|
444
795
|
// src/scheduler/task-runner.ts
|
|
445
796
|
var taskIdCounter = 0;
|
|
446
797
|
var TaskRunner = class {
|
|
@@ -742,7 +1093,7 @@ var Ahko = class {
|
|
|
742
1093
|
* ```
|
|
743
1094
|
*/
|
|
744
1095
|
constructor(options) {
|
|
745
|
-
this.queue = new TaskQueue(options?.concurrency);
|
|
1096
|
+
this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
|
|
746
1097
|
}
|
|
747
1098
|
/**
|
|
748
1099
|
* Schedules a task for execution with full return type inference.
|
|
@@ -754,6 +1105,7 @@ var Ahko = class {
|
|
|
754
1105
|
*
|
|
755
1106
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
756
1107
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1108
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
757
1109
|
*
|
|
758
1110
|
* @example
|
|
759
1111
|
* ```typescript
|
|
@@ -771,6 +1123,47 @@ var Ahko = class {
|
|
|
771
1123
|
if (typeof task !== "function") {
|
|
772
1124
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
773
1125
|
}
|
|
1126
|
+
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
1127
|
+
if (strategy === "debounce" /* DEBOUNCE */) {
|
|
1128
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1129
|
+
throw new AhkoConfigurationError(
|
|
1130
|
+
`Strategy "debounce" requires a valid "key" of type string or symbol.`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1134
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1135
|
+
throw new AhkoConfigurationError(
|
|
1136
|
+
`Strategy "debounce" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return this.queue.debounceCoordinator.schedule(
|
|
1140
|
+
options.key,
|
|
1141
|
+
task,
|
|
1142
|
+
waitMs,
|
|
1143
|
+
options,
|
|
1144
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
if (strategy === "throttle" /* THROTTLE */) {
|
|
1148
|
+
if (!options?.key || typeof options.key !== "string" && typeof options.key !== "symbol") {
|
|
1149
|
+
throw new AhkoConfigurationError(
|
|
1150
|
+
`Strategy "throttle" requires a valid "key" of type string or symbol.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const waitMs = options.waitMs ?? options.delay;
|
|
1154
|
+
if (typeof waitMs !== "number" || Number.isNaN(waitMs) || !Number.isFinite(waitMs) || waitMs < 0) {
|
|
1155
|
+
throw new AhkoConfigurationError(
|
|
1156
|
+
`Strategy "throttle" requires a non-negative finite "waitMs" or "delay" in milliseconds.`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
return this.queue.throttleCoordinator.schedule(
|
|
1160
|
+
options.key,
|
|
1161
|
+
task,
|
|
1162
|
+
waitMs,
|
|
1163
|
+
options,
|
|
1164
|
+
(t, opts) => this.schedule(t, { ...opts, strategy: "immediate" /* IMMEDIATE */ })
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
774
1167
|
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
775
1168
|
return this.queue.enqueue(runner, options);
|
|
776
1169
|
}
|
|
@@ -789,13 +1182,6 @@ var Ahko = class {
|
|
|
789
1182
|
*
|
|
790
1183
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
791
1184
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
792
|
-
*
|
|
793
|
-
* @example
|
|
794
|
-
* ```typescript
|
|
795
|
-
* const result = await ahko.idle(async ({ signal }) => {
|
|
796
|
-
* return computeAnalytics();
|
|
797
|
-
* });
|
|
798
|
-
* ```
|
|
799
1185
|
*/
|
|
800
1186
|
idle(task, options) {
|
|
801
1187
|
return this.schedule(task, {
|
|
@@ -803,6 +1189,42 @@ var Ahko = class {
|
|
|
803
1189
|
strategy: "idle" /* IDLE */
|
|
804
1190
|
});
|
|
805
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Convenience method to schedule a debounced task with key-based Promise coalescing.
|
|
1194
|
+
*
|
|
1195
|
+
* @template T - Inferred return type of the task.
|
|
1196
|
+
* @param key - Explicit identity key.
|
|
1197
|
+
* @param task - Work to execute once calls stop arriving.
|
|
1198
|
+
* @param waitMs - Quiet window duration in milliseconds.
|
|
1199
|
+
* @param options - Additional schedule options.
|
|
1200
|
+
* @returns Shared promise resolving with the final execution outcome.
|
|
1201
|
+
*/
|
|
1202
|
+
debounce(key, task, waitMs, options) {
|
|
1203
|
+
return this.schedule(task, {
|
|
1204
|
+
...options,
|
|
1205
|
+
strategy: "debounce" /* DEBOUNCE */,
|
|
1206
|
+
key,
|
|
1207
|
+
waitMs
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Convenience method to schedule a throttled task with leading execution and coalesced trailing run.
|
|
1212
|
+
*
|
|
1213
|
+
* @template T - Inferred return type of the task.
|
|
1214
|
+
* @param key - Explicit identity key.
|
|
1215
|
+
* @param task - Work to execute.
|
|
1216
|
+
* @param waitMs - Throttle interval duration in milliseconds.
|
|
1217
|
+
* @param options - Additional schedule options.
|
|
1218
|
+
* @returns Promise resolving with the leading or coalesced trailing result.
|
|
1219
|
+
*/
|
|
1220
|
+
throttle(key, task, waitMs, options) {
|
|
1221
|
+
return this.schedule(task, {
|
|
1222
|
+
...options,
|
|
1223
|
+
strategy: "throttle" /* THROTTLE */,
|
|
1224
|
+
key,
|
|
1225
|
+
waitMs
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
806
1228
|
/**
|
|
807
1229
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
808
1230
|
*
|
|
@@ -820,7 +1242,7 @@ var Ahko = class {
|
|
|
820
1242
|
};
|
|
821
1243
|
|
|
822
1244
|
// src/version.ts
|
|
823
|
-
var VERSION = "0.
|
|
1245
|
+
var VERSION = "0.5.0";
|
|
824
1246
|
|
|
825
1247
|
// src/errors/queue.error.ts
|
|
826
1248
|
var AhkoQueueError = class extends AhkoError {
|