@mrjacket/ahko 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-09-23 — Telemetry & DX
9
+
10
+ ### Added
11
+ - Type-safe lifecycle event emitter (`AhkoEventEmitter`) with dedicated `IAhkoEventMap` events:
12
+ - `task:start`: emitted when a task begins execution with `taskId` and `attempt`.
13
+ - `task:complete`: emitted on task success with `taskId`, `attempt`, `durationMs`, and `result`.
14
+ - `task:fail`: emitted on failure with `taskId`, `attempt`, `error`, and `willRetry` boolean indicator.
15
+ - `task:cancel`: emitted on task cancellation with `taskId` and `reason`.
16
+ - `task:timeout`: emitted when execution exceeds configured deadline with `taskId` and `timeoutMs`.
17
+ - `idle`: emitted when all tasks settle and queue reaches idle state with `timestamp`.
18
+ - Event subscription methods `ahko.on(event, handler)` returning an unsubscribe function, and `ahko.off(event, handler)`.
19
+ - Listener error containment: listener exceptions are safely isolated without crashing the scheduler loop or sibling listeners.
20
+ - Idle lifecycle promises via `ahko.isIdle()`, `ahko.onIdle()`, and chill alias `ahko.chill()`.
21
+ - Queue clearance method `ahko.clear()` to cancel queued, delayed, and coalesced tasks cleanly.
22
+ - Ahko mascot battery telemetry via `ahko.battery()` reporting chill status.
23
+ - Extended telemetry snapshot in `ahko.stats()` with `retriedTasks` and `totalDispatched`.
24
+
25
+ ### Fixed
26
+ - Fixed 2 CodeQL security alerts by iterating over map values (`this.entries.values()`) in `DebounceCoordinator.clear()` and `ThrottleCoordinator.clear()`.
27
+
28
+ ---
29
+
30
+ ## [0.5.0] - 2026-09-22 — Throttle, Debounce & Rate Limiting
31
+
32
+ ### Added
33
+ - Debounce scheduling strategy (`EScheduleStrategy.DEBOUNCE`) with quiet window timer resets.
34
+ - Throttle scheduling strategy (`EScheduleStrategy.THROTTLE`) with immediate leading execution and coalesced trailing run.
35
+ - Promise coalescing by explicit identity key (`key: string | symbol`): all concurrent callers awaiting the same key receive the exact same Promise resolution without artificial cancellation rejections.
36
+ - Task start interval rate limiting via `minIntervalMs` on scheduler constructor (`IAhkoOptions`).
37
+ - Automatic key cleanup and timer detachment upon settlement guaranteeing zero memory leaks.
38
+ - Convenience API methods `ahko.debounce()` and `ahko.throttle()`.
39
+ - Validation for keys, quiet windows, throttle periods, and rate limit intervals.
40
+ - Comprehensive unit test suites for debounce coalescing, throttle leading/trailing runs, and interval rate limiting.
41
+
42
+ ---
43
+
8
44
  ## [0.4.0] - 2026-09-22 — Timeout & Robust Cancellation
9
45
 
10
46
  ### Added
package/README.md CHANGED
@@ -27,6 +27,18 @@
27
27
  <a href="https://github.com/x-name15/ahko/blob/main/LICENSE">
28
28
  <img src="https://img.shields.io/npm/l/@mrjacket/ahko.svg" alt="license">
29
29
  </a>
30
+ <a href="https://www.npmjs.com/package/@mrjacket/ahko">
31
+ <img src="https://img.shields.io/badge/dependencies-0-success" alt="zero dependencies">
32
+ </a>
33
+ <a href="https://bundlephobia.com/package/@mrjacket/ahko">
34
+ <img src="https://img.shields.io/bundlephobia/minzip/@mrjacket/ahko?color=purple" alt="bundle size">
35
+ </a>
36
+ <a href="https://github.com/x-name15/ahko">
37
+ <img src="https://img.shields.io/badge/TypeScript-Ready-3178C6?logo=typescript&logoColor=white" alt="TypeScript">
38
+ </a>
39
+ <a href="https://github.com/x-name15/ahko/issues">
40
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome">
41
+ </a>
30
42
  </p>
31
43
 
32
44
  `ahko` is a low-energy task scheduler for JavaScript and TypeScript.
@@ -179,7 +191,32 @@ try {
179
191
  }
180
192
  ```
181
193
 
182
- ### 7. Telemetry (`stats`)
194
+ ### 7. Debounce & Throttle with Promise Coalescing
195
+
196
+ Coalesce repeated invocations into shared executions by explicit `key`. Callers share the exact same returned Promise:
197
+
198
+ ```typescript
199
+ // Debounce: waits for 300ms of quiet before running
200
+ const results = await ahko.debounce("search_box", async () => {
201
+ return queryApi(text);
202
+ }, 300);
203
+
204
+ // Throttle: runs leading edge immediately, coalesces trailing calls
205
+ await ahko.throttle("window_resize", async () => {
206
+ recalculateLayout();
207
+ }, 100);
208
+ ```
209
+
210
+ ### 8. Paced Execution (`minIntervalMs`)
211
+
212
+ Prevent burst spikes by ensuring a minimum interval elapses between consecutive task starts:
213
+
214
+ ```typescript
215
+ // At most 2 concurrent tasks, paced at least 50ms apart
216
+ const ahko = new Ahko({ concurrency: 2, minIntervalMs: 50 });
217
+ ```
218
+
219
+ ### 9. Telemetry (`stats`)
183
220
 
184
221
  Inspect real-time scheduler state without synthetic metrics:
185
222
 
@@ -194,10 +231,65 @@ console.log(stats);
194
231
  // failedTasks: 1,
195
232
  // cancelledTasks: 2,
196
233
  // timedOutTasks: 1,
234
+ // retriedTasks: 3,
235
+ // totalDispatched: 45,
197
236
  // capacity: 3
198
237
  // }
199
238
  ```
200
239
 
240
+ ### 10. Lifecycle Events (`on` / `off`)
241
+
242
+ Listen to typed lifecycle events with isolated callback safety:
243
+
244
+ ```typescript
245
+ const unsubscribe = ahko.on("task:start", ({ taskId, attempt }) => {
246
+ console.log(`Task ${taskId} started attempt #${attempt}`);
247
+ });
248
+
249
+ ahko.on("task:complete", ({ taskId, durationMs, result }) => {
250
+ console.log(`Task ${taskId} completed in ${durationMs}ms:`, result);
251
+ });
252
+
253
+ ahko.on("task:fail", ({ taskId, attempt, error, willRetry }) => {
254
+ console.warn(`Task ${taskId} attempt #${attempt} failed (willRetry: ${willRetry})`, error);
255
+ });
256
+
257
+ ahko.on("task:timeout", ({ taskId, timeoutMs }) => {
258
+ console.warn(`Task ${taskId} exceeded ${timeoutMs}ms deadline`);
259
+ });
260
+
261
+ ahko.on("task:cancel", ({ taskId, reason }) => {
262
+ console.info(`Task ${taskId} was cancelled:`, reason);
263
+ });
264
+
265
+ ahko.on("idle", ({ timestamp }) => {
266
+ console.log("Scheduler transitioned to idle at", timestamp);
267
+ });
268
+ ```
269
+
270
+ ### 11. Idle & Chill Developer Experience
271
+
272
+ Wait for all work to settle or clear the queue cleanly:
273
+
274
+ ```typescript
275
+ // Wait for all active and pending tasks to finish
276
+ await ahko.onIdle();
277
+ // Or use the completely chill alias:
278
+ await ahko.chill();
279
+
280
+ // Check if scheduler is currently idle
281
+ if (ahko.isIdle()) {
282
+ console.log("Completely chill. No tasks running or queued.");
283
+ }
284
+
285
+ // Clear all queued, delayed, and coalesced tasks
286
+ ahko.clear();
287
+
288
+ // Check Ahko mascot battery telemetry
289
+ console.log(ahko.battery());
290
+ // { level: 3, chill: true, status: "low-energy", quote: "Mwee... my battery is low, but all your tasks are handled completely chill." }
291
+ ```
292
+
201
293
  ---
202
294
 
203
295
  ## Documentation
@@ -206,10 +298,13 @@ Comprehensive guides and technical documentation are available in the [`docs/`](
206
298
 
207
299
  | Document | Description |
208
300
  |---|---|
209
- | [**Getting Started**](./docs/getting-started.md) | Quickstart guide, installation, and fundamental usage patterns. |
210
- | [**Library API**](./docs/library.md) | Complete programmatic API reference, TypeScript interfaces, and options. |
211
- | [**Architecture**](./docs/architecture.md) | Architectural specifications, lifecycle state machine, and design decisions. |
212
- | [**Roadmap**](./docs/roadmap.md) | Milestone progression from 0.1.0 through 1.0.0. |
301
+ | [**Documentation Portal**](./docs/README.md) | Master overview and index of all guides and specifications. |
302
+ | [**Getting Started**](./docs/guides/getting-started.md) | Quickstart guide, installation, and fundamental usage patterns. |
303
+ | [**Library API**](./docs/guides/library.md) | Complete programmatic API reference, TypeScript interfaces, and options. |
304
+ | [**Production Recipes**](./docs/guides/recipes.md) | Battle-tested recipes (paced API client, debounced search, throttled scroll, graceful shutdown). |
305
+ | [**Architecture**](./docs/architecture/ARCHITECTURE.md) | Architectural specifications, lifecycle state machine, and design decisions. |
306
+ | [**Roadmap**](./docs/architecture/ROADMAP.md) | Milestone progression from 0.1.0 through 1.0.0. |
307
+ | [**Engineering Log**](./docs/architecture/LOG.md) | Chronological log of engineering decisions and ADRs. |
213
308
 
214
309
  ---
215
310
 
@@ -222,6 +317,7 @@ Creates an AHKO scheduler instance.
222
317
  | Option | Type | Default | Description |
223
318
  |---|---|---|---|
224
319
  | `concurrency` | `number` | `Infinity` | Maximum concurrent tasks allowed to run simultaneously. Must be $\ge 1$. |
320
+ | `minIntervalMs` | `number` | `0` | Minimum interval in milliseconds between consecutive task starts. Must be $\ge 0$. |
225
321
 
226
322
  ### `ahko.schedule<T>(task: ITask<T>, options?: IScheduleOptions): Promise<T>`
227
323
 
@@ -229,20 +325,58 @@ Schedules an asynchronous task with full return type inference.
229
325
 
230
326
  | Option | Type | Default | Description |
231
327
  |---|---|---|---|
232
- | `strategy` | `"immediate" \| "delay" \| "idle"` | `"immediate"` | Scheduling execution strategy. |
328
+ | `strategy` | `"immediate" \| "delay" \| "idle" \| "throttle" \| "debounce"` | `"immediate"` | Scheduling execution strategy. |
233
329
  | `delay` | `number` | `0` | Delay in milliseconds when strategy is `"delay"`. |
330
+ | `key` | `string \| symbol` | `undefined` | Explicit identity key for `"debounce"` and `"throttle"`. |
331
+ | `waitMs` | `number` | `undefined` | Window duration in ms for debounce quiet period or throttle interval. |
234
332
  | `idleTimeout` | `number` | `undefined` | Maximum time to wait for idle window before forcing queue entry. |
235
333
  | `retry` | `IRetryOptions` | `undefined` | Automatic retry policy (attempts, backoff, jitter, predicate). |
236
334
  | `timeoutMs` | `number` | `undefined` | Maximum execution duration in milliseconds per attempt before aborting with `AhkoTimeoutError`. |
237
335
  | `signal` | `AbortSignal` | `undefined` | Optional external `AbortSignal` for cooperative cancellation. |
238
336
 
337
+ ### `ahko.debounce<T>(key: string | symbol, task: ITask<T>, waitMs: number, options?: IScheduleOptions): Promise<T>`
338
+
339
+ Convenience method scheduling a debounced task with key-based Promise coalescing.
340
+
341
+ ### `ahko.throttle<T>(key: string | symbol, task: ITask<T>, waitMs: number, options?: IScheduleOptions): Promise<T>`
342
+
343
+ Convenience method scheduling a throttled task with leading execution and coalesced trailing run.
344
+
239
345
  ### `ahko.idle<T>(task: ITask<T>, options?: Omit<IScheduleOptions, "strategy">): Promise<T>`
240
346
 
241
347
  Convenience method scheduling a task under `strategy: "idle"`.
242
348
 
349
+ ### `ahko.on(event, handler)`
350
+
351
+ Subscribes to scheduler lifecycle events (`task:start`, `task:complete`, `task:fail`, `task:cancel`, `task:timeout`, `idle`). Returns an unsubscribe function.
352
+
353
+ ### `ahko.off(event, handler)`
354
+
355
+ Unsubscribes an event listener callback.
356
+
357
+ ### `ahko.isIdle(): boolean`
358
+
359
+ Returns whether the scheduler is currently idle (no active or pending tasks).
360
+
361
+ ### `ahko.onIdle(): Promise<void>`
362
+
363
+ Returns a Promise that resolves when all active and pending tasks have settled.
364
+
365
+ ### `ahko.chill(): Promise<void>`
366
+
367
+ Alias for `ahko.onIdle()`.
368
+
369
+ ### `ahko.clear(): void`
370
+
371
+ Cancels all pending, delayed, and throttled/debounced tasks cleanly.
372
+
373
+ ### `ahko.battery()`
374
+
375
+ Returns mascot battery status and quote.
376
+
243
377
  ### `ahko.stats(): IAhkoStats`
244
378
 
245
- Returns a snapshot of current task counters and queue capacity.
379
+ Returns a snapshot of current task counters, retry counts, total dispatches, and queue capacity.
246
380
 
247
381
  ---
248
382
 
package/dist/ahko.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TAhkoEventName, TAhkoEventHandler, TAhkoUnsubscribe } from "./models/events.model.js";
1
2
  import type { IAhkoOptions, IScheduleOptions } from "./models/options.model.js";
2
3
  import type { IAhkoStats } from "./models/stats.model.js";
3
4
  import type { ITask } from "./models/task.model.js";
@@ -44,6 +45,7 @@ export declare class Ahko {
44
45
  *
45
46
  * @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
46
47
  * @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
48
+ * @throws {AhkoTimeoutError} If task execution exceeds timeoutMs.
47
49
  *
48
50
  * @example
49
51
  * ```typescript
@@ -73,15 +75,30 @@ export declare class Ahko {
73
75
  *
74
76
  * @throws {AhkoConfigurationError} If the task is not a function or options are invalid.
75
77
  * @throws {AhkoCancellationError} If the task is cancelled prior to or during execution.
76
- *
77
- * @example
78
- * ```typescript
79
- * const result = await ahko.idle(async ({ signal }) => {
80
- * return computeAnalytics();
81
- * });
82
- * ```
83
78
  */
84
79
  idle<T>(task: ITask<T>, options?: Omit<IScheduleOptions, "strategy">): Promise<T>;
80
+ /**
81
+ * Convenience method to schedule a debounced task with key-based Promise coalescing.
82
+ *
83
+ * @template T - Inferred return type of the task.
84
+ * @param key - Explicit identity key.
85
+ * @param task - Work to execute once calls stop arriving.
86
+ * @param waitMs - Quiet window duration in milliseconds.
87
+ * @param options - Additional schedule options.
88
+ * @returns Shared promise resolving with the final execution outcome.
89
+ */
90
+ debounce<T>(key: string | symbol, task: ITask<T>, waitMs: number, options?: Omit<IScheduleOptions, "strategy" | "key" | "waitMs">): Promise<T>;
91
+ /**
92
+ * Convenience method to schedule a throttled task with leading execution and coalesced trailing run.
93
+ *
94
+ * @template T - Inferred return type of the task.
95
+ * @param key - Explicit identity key.
96
+ * @param task - Work to execute.
97
+ * @param waitMs - Throttle interval duration in milliseconds.
98
+ * @param options - Additional schedule options.
99
+ * @returns Promise resolving with the leading or coalesced trailing result.
100
+ */
101
+ throttle<T>(key: string | symbol, task: ITask<T>, waitMs: number, options?: Omit<IScheduleOptions, "strategy" | "key" | "waitMs">): Promise<T>;
85
102
  /**
86
103
  * Retrieves real-time telemetry metrics from the scheduler.
87
104
  *
@@ -94,4 +111,67 @@ export declare class Ahko {
94
111
  * ```
95
112
  */
96
113
  stats(): IAhkoStats;
114
+ /**
115
+ * Subscribes to a scheduler lifecycle event.
116
+ *
117
+ * @param event - Event name to listen for.
118
+ * @param handler - Callback function invoked when the event is emitted.
119
+ * @returns Unsubscribe function to remove the listener.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const unsubscribe = ahko.on("task:start", ({ taskId, attempt }) => {
124
+ * console.log(`Task ${taskId} started attempt ${attempt}`);
125
+ * });
126
+ * ```
127
+ */
128
+ on<K extends TAhkoEventName>(event: K, handler: TAhkoEventHandler<K>): TAhkoUnsubscribe;
129
+ /**
130
+ * Unsubscribes an event listener from a scheduler lifecycle event.
131
+ *
132
+ * @param event - Event name.
133
+ * @param handler - The exact listener callback to remove.
134
+ */
135
+ off<K extends TAhkoEventName>(event: K, handler: TAhkoEventHandler<K>): void;
136
+ /**
137
+ * Checks whether the scheduler is currently idle (no active runners and no pending tasks).
138
+ *
139
+ * @returns True if completely idle, false otherwise.
140
+ */
141
+ isIdle(): boolean;
142
+ /**
143
+ * Returns a promise that resolves once the scheduler has completed all tasks and is idle.
144
+ *
145
+ * @returns Promise resolving when the scheduler is idle.
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * ahko.schedule(doWork);
150
+ * await ahko.onIdle();
151
+ * console.log("All work finished!");
152
+ * ```
153
+ */
154
+ onIdle(): Promise<void>;
155
+ /**
156
+ * Clears all pending, delayed, and throttled/debounced tasks from the scheduler.
157
+ * In-flight active tasks will continue executing to completion or abort via signal.
158
+ */
159
+ clear(): void;
160
+ /**
161
+ * Returns the delightful Ahko mascot battery telemetry status.
162
+ *
163
+ * Low energy, completely chill.
164
+ */
165
+ battery(): {
166
+ level: number;
167
+ chill: boolean;
168
+ status: string;
169
+ quote: string;
170
+ };
171
+ /**
172
+ * Delightful alias for `onIdle()`: wait for all tasks to settle chill and relaxed.
173
+ *
174
+ * @returns Promise resolving when all tasks have finished.
175
+ */
176
+ chill(): Promise<void>;
97
177
  }
@@ -0,0 +1,34 @@
1
+ import type { IAhkoEventMap, TAhkoEventHandler, TAhkoEventName, TAhkoUnsubscribe } from "../models/events.model.js";
2
+ /**
3
+ * Lightweight, zero-dependency typed event emitter with safe error containment.
4
+ */
5
+ export declare class AhkoEventEmitter {
6
+ private readonly listeners;
7
+ /**
8
+ * Subscribes a listener to a specific Ahko lifecycle event.
9
+ *
10
+ * @param event - The event name to subscribe to.
11
+ * @param handler - The callback function to invoke when the event is emitted.
12
+ * @returns An unsubscribe function to remove the listener.
13
+ */
14
+ on<K extends TAhkoEventName>(event: K, handler: TAhkoEventHandler<K>): TAhkoUnsubscribe;
15
+ /**
16
+ * Unsubscribes a listener from a specific Ahko lifecycle event.
17
+ *
18
+ * @param event - The event name.
19
+ * @param handler - The callback function to remove.
20
+ */
21
+ off<K extends TAhkoEventName>(event: K, handler: TAhkoEventHandler<K>): void;
22
+ /**
23
+ * Emits an event with the corresponding typed payload to all subscribed listeners.
24
+ * Listener invocations are safely isolated in try/catch to protect scheduler integrity.
25
+ *
26
+ * @param event - The event name to emit.
27
+ * @param payload - The event-specific payload data.
28
+ */
29
+ emit<K extends TAhkoEventName>(event: K, payload: IAhkoEventMap[K]): void;
30
+ /**
31
+ * Removes all registered event listeners.
32
+ */
33
+ clear(): void;
34
+ }
@@ -0,0 +1 @@
1
+ export * from "./event-emitter.js";