@mrjacket/ahko 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +59 -3
- package/dist/ahko.d.ts +23 -7
- package/dist/errors/timeout.error.d.ts +15 -2
- package/dist/index.cjs +651 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +649 -54
- package/dist/index.js.map +1 -1
- package/dist/models/options.model.d.ts +24 -0
- package/dist/models/strategy.model.d.ts +6 -2
- package/dist/scheduler/debounce-coordinator.d.ts +41 -0
- package/dist/scheduler/signal.d.ts +20 -0
- package/dist/scheduler/task-queue.d.ts +16 -3
- package/dist/scheduler/task-runner.d.ts +17 -8
- package/dist/scheduler/throttle-coordinator.d.ts +43 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,9 +33,31 @@ 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
|
|
|
41
|
+
// src/errors/timeout.error.ts
|
|
42
|
+
var AhkoTimeoutError = class extends AhkoError {
|
|
43
|
+
/**
|
|
44
|
+
* The timeout threshold in milliseconds that was exceeded, if configured.
|
|
45
|
+
*/
|
|
46
|
+
timeoutMs;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a new AhkoTimeoutError.
|
|
49
|
+
*
|
|
50
|
+
* @param message - Explanation of timeout expiry.
|
|
51
|
+
* @param options - Standard Error options including optional timeoutMs and cause.
|
|
52
|
+
*/
|
|
53
|
+
constructor(message = "Task execution timed out", options) {
|
|
54
|
+
super(message, options);
|
|
55
|
+
this.name = "AhkoTimeoutError";
|
|
56
|
+
this.timeoutMs = options?.timeoutMs;
|
|
57
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
39
61
|
// src/models/state.model.ts
|
|
40
62
|
var ETaskState = /* @__PURE__ */ ((ETaskState2) => {
|
|
41
63
|
ETaskState2["PENDING"] = "pending";
|
|
@@ -72,6 +94,163 @@ function calculateBackoff(attempt, options, randomFn = Math.random) {
|
|
|
72
94
|
return Math.floor(cappedDelay);
|
|
73
95
|
}
|
|
74
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
|
+
|
|
75
254
|
// src/scheduler/idle-scheduler.ts
|
|
76
255
|
var IdleScheduler = class {
|
|
77
256
|
/**
|
|
@@ -111,10 +290,150 @@ var IdleScheduler = class {
|
|
|
111
290
|
}
|
|
112
291
|
};
|
|
113
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
|
+
|
|
114
427
|
// src/scheduler/task-queue.ts
|
|
115
428
|
var TaskQueue = class {
|
|
116
429
|
/** Maximum concurrent active tasks */
|
|
117
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;
|
|
118
437
|
/** Queue of pending task runners waiting for a concurrency slot */
|
|
119
438
|
queue = [];
|
|
120
439
|
/** Set of task runners currently executing */
|
|
@@ -125,6 +444,10 @@ var TaskQueue = class {
|
|
|
125
444
|
idleEntries = /* @__PURE__ */ new Set();
|
|
126
445
|
/** Set of tasks currently awaiting a retry backoff timer */
|
|
127
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();
|
|
128
451
|
/** WeakMap associating task runners with their scheduling options */
|
|
129
452
|
runnerOptions = /* @__PURE__ */ new WeakMap();
|
|
130
453
|
/** Cumulative completed tasks counter */
|
|
@@ -139,15 +462,22 @@ var TaskQueue = class {
|
|
|
139
462
|
* Creates a new TaskQueue.
|
|
140
463
|
*
|
|
141
464
|
* @param concurrency - Maximum concurrent tasks (defaults to Infinity).
|
|
142
|
-
* @
|
|
465
|
+
* @param minIntervalMs - Minimum interval in milliseconds between task dispatches.
|
|
466
|
+
* @throws {AhkoConfigurationError} If concurrency is less than 1 or minIntervalMs is invalid.
|
|
143
467
|
*/
|
|
144
|
-
constructor(concurrency = Infinity) {
|
|
468
|
+
constructor(concurrency = Infinity, minIntervalMs = 0) {
|
|
145
469
|
if (Number.isNaN(concurrency) || concurrency < 1) {
|
|
146
470
|
throw new AhkoConfigurationError(
|
|
147
471
|
`Invalid concurrency "${concurrency}". Must be a number greater than or equal to 1.`
|
|
148
472
|
);
|
|
149
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
|
+
}
|
|
150
479
|
this.concurrency = concurrency;
|
|
480
|
+
this.minIntervalMs = minIntervalMs;
|
|
151
481
|
}
|
|
152
482
|
/**
|
|
153
483
|
* Enqueues a task runner according to the specified schedule options.
|
|
@@ -160,11 +490,24 @@ var TaskQueue = class {
|
|
|
160
490
|
*/
|
|
161
491
|
enqueue(runner, options) {
|
|
162
492
|
const strategy = options?.strategy ?? "immediate" /* IMMEDIATE */;
|
|
163
|
-
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 */) {
|
|
164
494
|
throw new AhkoConfigurationError(
|
|
165
|
-
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle".`
|
|
495
|
+
`Unsupported schedule strategy "${String(strategy)}". Supported strategies: "immediate", "delay", "idle", "throttle", "debounce".`
|
|
166
496
|
);
|
|
167
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
|
+
}
|
|
168
511
|
if (options?.retry) {
|
|
169
512
|
if (typeof options.retry.attempts !== "number" || Number.isNaN(options.retry.attempts) || options.retry.attempts < 1 || !Number.isInteger(options.retry.attempts)) {
|
|
170
513
|
throw new AhkoConfigurationError(
|
|
@@ -182,6 +525,13 @@ var TaskQueue = class {
|
|
|
182
525
|
);
|
|
183
526
|
}
|
|
184
527
|
}
|
|
528
|
+
if (options?.timeoutMs !== void 0) {
|
|
529
|
+
if (typeof options.timeoutMs !== "number" || Number.isNaN(options.timeoutMs) || !Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
530
|
+
throw new AhkoConfigurationError(
|
|
531
|
+
`Invalid timeoutMs "${options.timeoutMs}". timeoutMs must be a positive finite number greater than 0.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
185
535
|
if (options) {
|
|
186
536
|
this.runnerOptions.set(runner, options);
|
|
187
537
|
}
|
|
@@ -284,10 +634,41 @@ var TaskQueue = class {
|
|
|
284
634
|
}
|
|
285
635
|
/**
|
|
286
636
|
* Pumps the queue by picking pending tasks and executing them
|
|
287
|
-
* as long as concurrency capacity is available.
|
|
637
|
+
* as long as concurrency capacity is available and minIntervalMs is respected.
|
|
288
638
|
*/
|
|
289
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
|
+
}
|
|
290
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
|
+
}
|
|
291
672
|
const runner = this.queue.shift();
|
|
292
673
|
if (!runner) {
|
|
293
674
|
break;
|
|
@@ -296,7 +677,19 @@ var TaskQueue = class {
|
|
|
296
677
|
continue;
|
|
297
678
|
}
|
|
298
679
|
this.activeRunners.add(runner);
|
|
680
|
+
this.lastTaskStartTime = Date.now();
|
|
299
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
|
+
}
|
|
300
693
|
}
|
|
301
694
|
}
|
|
302
695
|
/**
|
|
@@ -324,7 +717,7 @@ var TaskQueue = class {
|
|
|
324
717
|
this.scheduleRetry(runner, options);
|
|
325
718
|
return;
|
|
326
719
|
}
|
|
327
|
-
if (runner.state === "timed_out" /* TIMED_OUT */) {
|
|
720
|
+
if (runner.state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
328
721
|
this.timedOutTasks++;
|
|
329
722
|
} else {
|
|
330
723
|
this.failedTasks++;
|
|
@@ -389,7 +782,7 @@ var TaskQueue = class {
|
|
|
389
782
|
getStats() {
|
|
390
783
|
return Object.freeze({
|
|
391
784
|
activeTasks: this.activeRunners.size,
|
|
392
|
-
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,
|
|
393
786
|
completedTasks: this.completedTasks,
|
|
394
787
|
failedTasks: this.failedTasks,
|
|
395
788
|
cancelledTasks: this.cancelledTasks,
|
|
@@ -399,21 +792,6 @@ var TaskQueue = class {
|
|
|
399
792
|
}
|
|
400
793
|
};
|
|
401
794
|
|
|
402
|
-
// src/errors/cancellation.error.ts
|
|
403
|
-
var AhkoCancellationError = class extends AhkoError {
|
|
404
|
-
/**
|
|
405
|
-
* Creates a new AhkoCancellationError.
|
|
406
|
-
*
|
|
407
|
-
* @param message - Reason for cancellation.
|
|
408
|
-
* @param options - Standard Error options including cause.
|
|
409
|
-
*/
|
|
410
|
-
constructor(message = "Task was cancelled", options) {
|
|
411
|
-
super(message, options);
|
|
412
|
-
this.name = "AhkoCancellationError";
|
|
413
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
795
|
// src/scheduler/task-runner.ts
|
|
418
796
|
var taskIdCounter = 0;
|
|
419
797
|
var TaskRunner = class {
|
|
@@ -427,6 +805,10 @@ var TaskRunner = class {
|
|
|
427
805
|
task;
|
|
428
806
|
/** User-supplied AbortSignal for external cancellation */
|
|
429
807
|
externalSignal;
|
|
808
|
+
/** Maximum execution duration allowed in milliseconds */
|
|
809
|
+
timeoutMs;
|
|
810
|
+
/** Active timeout timer identifier */
|
|
811
|
+
timeoutTimerId;
|
|
430
812
|
/** Abort event listener reference for clean detachment */
|
|
431
813
|
abortListener;
|
|
432
814
|
/** Promise resolve handler */
|
|
@@ -437,16 +819,20 @@ var TaskRunner = class {
|
|
|
437
819
|
promise;
|
|
438
820
|
/** Callback invoked when runner is cancelled while pending */
|
|
439
821
|
onCancel;
|
|
822
|
+
/** Current execution attempt count (1-indexed) */
|
|
823
|
+
attempt = 1;
|
|
440
824
|
/**
|
|
441
825
|
* Creates a new TaskRunner instance.
|
|
442
826
|
*
|
|
443
827
|
* @param task - The asynchronous work unit to run.
|
|
444
828
|
* @param externalSignal - Optional external AbortSignal to propagate.
|
|
829
|
+
* @param timeoutMs - Optional maximum execution time in milliseconds.
|
|
445
830
|
*/
|
|
446
|
-
constructor(task, externalSignal) {
|
|
831
|
+
constructor(task, externalSignal, timeoutMs) {
|
|
447
832
|
this.taskId = `task_${Date.now().toString(36)}_${(++taskIdCounter).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
448
833
|
this.task = task;
|
|
449
834
|
this.externalSignal = externalSignal;
|
|
835
|
+
this.timeoutMs = timeoutMs;
|
|
450
836
|
this.abortController = new AbortController();
|
|
451
837
|
this.promise = new Promise((resolve, reject) => {
|
|
452
838
|
this.resolvePromise = resolve;
|
|
@@ -460,6 +846,7 @@ var TaskRunner = class {
|
|
|
460
846
|
typeof reason === "string" ? reason : "Task was cancelled prior to execution",
|
|
461
847
|
{ cause: reason instanceof Error ? reason : void 0 }
|
|
462
848
|
);
|
|
849
|
+
this.abortController.abort(cancelError);
|
|
463
850
|
this.rejectPromise(cancelError);
|
|
464
851
|
} else {
|
|
465
852
|
this.abortListener = () => {
|
|
@@ -475,8 +862,6 @@ var TaskRunner = class {
|
|
|
475
862
|
get state() {
|
|
476
863
|
return this._state;
|
|
477
864
|
}
|
|
478
|
-
/** Current execution attempt count (1-indexed) */
|
|
479
|
-
attempt = 1;
|
|
480
865
|
/**
|
|
481
866
|
* Resolves the deferred promise.
|
|
482
867
|
*
|
|
@@ -496,14 +881,14 @@ var TaskRunner = class {
|
|
|
496
881
|
this.rejectPromise(reason);
|
|
497
882
|
}
|
|
498
883
|
/**
|
|
499
|
-
* Evaluates if the task should be retried following an execution failure.
|
|
884
|
+
* Evaluates if the task should be retried following an execution failure or timeout.
|
|
500
885
|
*
|
|
501
886
|
* @param error - The error encountered during the attempt.
|
|
502
887
|
* @param retryOptions - Configured retry policy.
|
|
503
888
|
* @returns A promise resolving to true if retry should proceed, false otherwise.
|
|
504
889
|
*/
|
|
505
890
|
async canRetry(error, retryOptions) {
|
|
506
|
-
if (this._state === "cancelled" /* CANCELLED */ || this.
|
|
891
|
+
if (this._state === "cancelled" /* CANCELLED */ || (this.externalSignal?.aborted ?? false)) {
|
|
507
892
|
return false;
|
|
508
893
|
}
|
|
509
894
|
if (!retryOptions || typeof retryOptions.attempts !== "number") {
|
|
@@ -524,12 +909,13 @@ var TaskRunner = class {
|
|
|
524
909
|
}
|
|
525
910
|
this.attempt++;
|
|
526
911
|
this._state = "pending" /* PENDING */;
|
|
912
|
+
this.abortController = new AbortController();
|
|
527
913
|
return true;
|
|
528
914
|
}
|
|
529
915
|
/**
|
|
530
916
|
* Executes the task within an allocated concurrency slot.
|
|
531
917
|
*
|
|
532
|
-
* @returns A promise resolving to the task result or rejecting on failure/cancellation.
|
|
918
|
+
* @returns A promise resolving to the task result or rejecting on failure/cancellation/timeout.
|
|
533
919
|
*/
|
|
534
920
|
async run() {
|
|
535
921
|
if (this._state === "cancelled" /* CANCELLED */) {
|
|
@@ -540,14 +926,100 @@ var TaskRunner = class {
|
|
|
540
926
|
signal: this.abortController.signal,
|
|
541
927
|
taskId: this.taskId
|
|
542
928
|
};
|
|
929
|
+
let abortListener;
|
|
930
|
+
const abortPromise = new Promise((_, reject) => {
|
|
931
|
+
abortListener = () => {
|
|
932
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
933
|
+
reject(
|
|
934
|
+
new AhkoTimeoutError(
|
|
935
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
936
|
+
{ timeoutMs: this.timeoutMs }
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
} else {
|
|
940
|
+
const reason = this.abortController.signal.reason;
|
|
941
|
+
reject(
|
|
942
|
+
new AhkoCancellationError("Task was cancelled during execution", {
|
|
943
|
+
cause: reason instanceof Error ? reason : void 0
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
this.abortController.signal.addEventListener("abort", abortListener, { once: true });
|
|
949
|
+
});
|
|
950
|
+
let timeoutPromise;
|
|
951
|
+
if (this.timeoutMs !== void 0) {
|
|
952
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
953
|
+
this.timeoutTimerId = setTimeout(() => {
|
|
954
|
+
if (this._state !== "running" /* RUNNING */) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
958
|
+
const timeoutError = new AhkoTimeoutError(
|
|
959
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
960
|
+
{ timeoutMs: this.timeoutMs }
|
|
961
|
+
);
|
|
962
|
+
this.abortController.abort(timeoutError);
|
|
963
|
+
reject(timeoutError);
|
|
964
|
+
}, this.timeoutMs);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
let taskExecutionPromise;
|
|
968
|
+
try {
|
|
969
|
+
taskExecutionPromise = Promise.resolve(this.task(context));
|
|
970
|
+
} catch (syncError) {
|
|
971
|
+
taskExecutionPromise = Promise.reject(syncError);
|
|
972
|
+
}
|
|
973
|
+
taskExecutionPromise.catch(() => {
|
|
974
|
+
});
|
|
975
|
+
const racePromises = [
|
|
976
|
+
taskExecutionPromise,
|
|
977
|
+
abortPromise
|
|
978
|
+
];
|
|
979
|
+
if (timeoutPromise) {
|
|
980
|
+
racePromises.push(timeoutPromise);
|
|
981
|
+
}
|
|
543
982
|
try {
|
|
544
|
-
const result = await
|
|
983
|
+
const result = await Promise.race(racePromises);
|
|
984
|
+
this.clearTimeoutTimer();
|
|
985
|
+
if (abortListener) {
|
|
986
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
987
|
+
}
|
|
988
|
+
if (this._state === "timed_out" /* TIMED_OUT */) {
|
|
989
|
+
throw new AhkoTimeoutError(
|
|
990
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
991
|
+
{ timeoutMs: this.timeoutMs }
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (this._state === "cancelled" /* CANCELLED */) {
|
|
995
|
+
throw new AhkoCancellationError("Task was cancelled during execution");
|
|
996
|
+
}
|
|
545
997
|
this._state = "completed" /* COMPLETED */;
|
|
546
998
|
return result;
|
|
547
999
|
} catch (error) {
|
|
548
|
-
|
|
1000
|
+
this.clearTimeoutTimer();
|
|
1001
|
+
if (abortListener) {
|
|
1002
|
+
this.abortController.signal.removeEventListener("abort", abortListener);
|
|
1003
|
+
}
|
|
1004
|
+
if (this._state === "timed_out" /* TIMED_OUT */ || error instanceof AhkoTimeoutError) {
|
|
1005
|
+
this._state = "timed_out" /* TIMED_OUT */;
|
|
1006
|
+
if (error instanceof AhkoTimeoutError) {
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
throw new AhkoTimeoutError(
|
|
1010
|
+
`Task execution timed out after ${this.timeoutMs}ms`,
|
|
1011
|
+
{
|
|
1012
|
+
timeoutMs: this.timeoutMs,
|
|
1013
|
+
cause: error instanceof Error ? error : void 0
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
const isCancelled = this._state === "cancelled" /* CANCELLED */ || this.abortController.signal.aborted || (this.externalSignal?.aborted ?? false);
|
|
549
1018
|
if (isCancelled) {
|
|
550
1019
|
this._state = "cancelled" /* CANCELLED */;
|
|
1020
|
+
if (error instanceof AhkoCancellationError) {
|
|
1021
|
+
throw error;
|
|
1022
|
+
}
|
|
551
1023
|
throw new AhkoCancellationError("Task was cancelled during execution", {
|
|
552
1024
|
cause: error instanceof Error ? error : void 0
|
|
553
1025
|
});
|
|
@@ -556,6 +1028,15 @@ var TaskRunner = class {
|
|
|
556
1028
|
throw error;
|
|
557
1029
|
}
|
|
558
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Clears the active timeout timer.
|
|
1033
|
+
*/
|
|
1034
|
+
clearTimeoutTimer() {
|
|
1035
|
+
if (this.timeoutTimerId !== void 0) {
|
|
1036
|
+
clearTimeout(this.timeoutTimerId);
|
|
1037
|
+
this.timeoutTimerId = void 0;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
559
1040
|
/**
|
|
560
1041
|
* Cancels the task, aborting pending or running execution.
|
|
561
1042
|
*
|
|
@@ -567,6 +1048,7 @@ var TaskRunner = class {
|
|
|
567
1048
|
}
|
|
568
1049
|
const wasPending = this._state === "pending" /* PENDING */;
|
|
569
1050
|
this._state = "cancelled" /* CANCELLED */;
|
|
1051
|
+
this.clearTimeoutTimer();
|
|
570
1052
|
this.abortController.abort(reason);
|
|
571
1053
|
this.cleanup();
|
|
572
1054
|
if (wasPending) {
|
|
@@ -588,6 +1070,7 @@ var TaskRunner = class {
|
|
|
588
1070
|
* Detaches event listeners from external signal to guarantee memory safety.
|
|
589
1071
|
*/
|
|
590
1072
|
cleanup() {
|
|
1073
|
+
this.clearTimeoutTimer();
|
|
591
1074
|
if (this.externalSignal && this.abortListener) {
|
|
592
1075
|
this.externalSignal.removeEventListener("abort", this.abortListener);
|
|
593
1076
|
}
|
|
@@ -610,7 +1093,7 @@ var Ahko = class {
|
|
|
610
1093
|
* ```
|
|
611
1094
|
*/
|
|
612
1095
|
constructor(options) {
|
|
613
|
-
this.queue = new TaskQueue(options?.concurrency);
|
|
1096
|
+
this.queue = new TaskQueue(options?.concurrency, options?.minIntervalMs);
|
|
614
1097
|
}
|
|
615
1098
|
/**
|
|
616
1099
|
* Schedules a task for execution with full return type inference.
|
|
@@ -622,6 +1105,7 @@ var Ahko = class {
|
|
|
622
1105
|
*
|
|
623
1106
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
624
1107
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
1108
|
+
* @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
|
|
625
1109
|
*
|
|
626
1110
|
* @example
|
|
627
1111
|
* ```typescript
|
|
@@ -639,7 +1123,48 @@ var Ahko = class {
|
|
|
639
1123
|
if (typeof task !== "function") {
|
|
640
1124
|
throw new AhkoConfigurationError("Task must be a valid function.");
|
|
641
1125
|
}
|
|
642
|
-
const
|
|
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
|
+
}
|
|
1167
|
+
const runner = new TaskRunner(task, options?.signal, options?.timeoutMs);
|
|
643
1168
|
return this.queue.enqueue(runner, options);
|
|
644
1169
|
}
|
|
645
1170
|
/**
|
|
@@ -657,13 +1182,6 @@ var Ahko = class {
|
|
|
657
1182
|
*
|
|
658
1183
|
* @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
|
|
659
1184
|
* @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
|
|
660
|
-
*
|
|
661
|
-
* @example
|
|
662
|
-
* ```typescript
|
|
663
|
-
* const result = await ahko.idle(async ({ signal }) => {
|
|
664
|
-
* return computeAnalytics();
|
|
665
|
-
* });
|
|
666
|
-
* ```
|
|
667
1185
|
*/
|
|
668
1186
|
idle(task, options) {
|
|
669
1187
|
return this.schedule(task, {
|
|
@@ -671,6 +1189,42 @@ var Ahko = class {
|
|
|
671
1189
|
strategy: "idle" /* IDLE */
|
|
672
1190
|
});
|
|
673
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
|
+
}
|
|
674
1228
|
/**
|
|
675
1229
|
* Retrieves real-time telemetry metrics from the scheduler.
|
|
676
1230
|
*
|
|
@@ -688,7 +1242,7 @@ var Ahko = class {
|
|
|
688
1242
|
};
|
|
689
1243
|
|
|
690
1244
|
// src/version.ts
|
|
691
|
-
var VERSION = "0.
|
|
1245
|
+
var VERSION = "0.5.0";
|
|
692
1246
|
|
|
693
1247
|
// src/errors/queue.error.ts
|
|
694
1248
|
var AhkoQueueError = class extends AhkoError {
|
|
@@ -705,20 +1259,60 @@ var AhkoQueueError = class extends AhkoError {
|
|
|
705
1259
|
}
|
|
706
1260
|
};
|
|
707
1261
|
|
|
708
|
-
// src/
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1262
|
+
// src/scheduler/signal.ts
|
|
1263
|
+
function combineSignals(signals) {
|
|
1264
|
+
const activeSignals = signals.filter(
|
|
1265
|
+
(signal) => signal !== void 0
|
|
1266
|
+
);
|
|
1267
|
+
if (activeSignals.length === 0) {
|
|
1268
|
+
const controller2 = new AbortController();
|
|
1269
|
+
return {
|
|
1270
|
+
signal: controller2.signal,
|
|
1271
|
+
cleanup: () => {
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
720
1274
|
}
|
|
721
|
-
|
|
1275
|
+
const alreadyAborted = activeSignals.find((s) => s.aborted);
|
|
1276
|
+
if (alreadyAborted) {
|
|
1277
|
+
const controller2 = new AbortController();
|
|
1278
|
+
controller2.abort(alreadyAborted.reason);
|
|
1279
|
+
return {
|
|
1280
|
+
signal: controller2.signal,
|
|
1281
|
+
cleanup: () => {
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
if (activeSignals.length === 1) {
|
|
1286
|
+
return {
|
|
1287
|
+
signal: activeSignals[0],
|
|
1288
|
+
cleanup: () => {
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const controller = new AbortController();
|
|
1293
|
+
const cleanupFns = [];
|
|
1294
|
+
const onAbort = (event) => {
|
|
1295
|
+
const target = event.target;
|
|
1296
|
+
cleanup();
|
|
1297
|
+
controller.abort(target.reason);
|
|
1298
|
+
};
|
|
1299
|
+
for (const sig of activeSignals) {
|
|
1300
|
+
sig.addEventListener("abort", onAbort, { once: true });
|
|
1301
|
+
cleanupFns.push(() => {
|
|
1302
|
+
sig.removeEventListener("abort", onAbort);
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
const cleanup = () => {
|
|
1306
|
+
for (const fn of cleanupFns) {
|
|
1307
|
+
fn();
|
|
1308
|
+
}
|
|
1309
|
+
cleanupFns.length = 0;
|
|
1310
|
+
};
|
|
1311
|
+
return {
|
|
1312
|
+
signal: controller.signal,
|
|
1313
|
+
cleanup
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
722
1316
|
export {
|
|
723
1317
|
Ahko,
|
|
724
1318
|
AhkoCancellationError,
|
|
@@ -731,6 +1325,7 @@ export {
|
|
|
731
1325
|
EScheduleStrategy,
|
|
732
1326
|
ETaskState,
|
|
733
1327
|
VERSION,
|
|
734
|
-
calculateBackoff
|
|
1328
|
+
calculateBackoff,
|
|
1329
|
+
combineSignals
|
|
735
1330
|
};
|
|
736
1331
|
//# sourceMappingURL=index.js.map
|