@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/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
- * @throws {AhkoConfigurationError} If concurrency is less than 1 or not a valid number.
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.4.0";
1245
+ var VERSION = "0.5.0";
824
1246
 
825
1247
  // src/errors/queue.error.ts
826
1248
  var AhkoQueueError = class extends AhkoError {