hookified 2.1.1 → 3.0.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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  # Features
13
13
  - Simple replacement for EventEmitter
14
14
  - Async / Sync Middleware Hooks for Your Methods
15
- - ESM / CJS with Types and Nodejs 20+
15
+ - ESM / CJS with Types
16
16
  - Browser Support and Delivered via CDN
17
17
  - Ability to throw errors in hooks
18
18
  - Ability to pass in a logger (such as Pino) for errors
@@ -20,6 +20,7 @@
20
20
  - Deprecation warnings for hooks with `deprecatedHooks`
21
21
  - Control deprecated hook execution with `allowDeprecated`
22
22
  - WaterfallHook for sequential data transformation pipelines
23
+ - ParallelHook for concurrent fan-out execution with collected results
23
24
  - No package dependencies and only 250KB in size
24
25
  - Fast and Efficient with [Benchmarks](#benchmarks)
25
26
  - Maintained on a regular basis!
@@ -27,11 +28,13 @@
27
28
  # Table of Contents
28
29
  - [Installation](#installation)
29
30
  - [Usage](#usage)
31
+ - [Migrating from v2 to v3](#migrating-from-v2-to-v3)
30
32
  - [Migrating from v1 to v2](#migrating-from-v1-to-v2)
31
33
  - [Using it in the Browser](#using-it-in-the-browser)
32
34
  - [Hooks](#hooks)
33
35
  - [Standard Hook](#standard-hook)
34
- - [Waterfall Hook](#waterfallhook)
36
+ - [Waterfall Hook](#waterfall-hook)
37
+ - [Parallel Hook](#parallel-hook)
35
38
  - [API - Hooks](#api---hooks)
36
39
  - [.allowDeprecated](#allowdeprecated)
37
40
  - [.deprecatedHooks](#deprecatedhooks)
@@ -342,6 +345,114 @@ wh.removeHook(myHook); // returns true
342
345
  console.log(wh.hooks.length); // 0
343
346
  ```
344
347
 
348
+ ## Parallel Hook
349
+
350
+ The `ParallelHook` class fans a single invocation out to many registered hook functions concurrently via `Promise.allSettled`, then calls a final handler with the aggregated outcomes — including failures. Unlike `WaterfallHook`, hooks do not see each other's results: every hook receives the same `initialArgs` and runs in parallel. It implements the `IHook` interface, so it integrates directly with `Hookified.onHook()`, and the final handler still fires whether the hook is invoked directly or through `Hookified.hook()`.
351
+
352
+ Per-hook functions receive a `ParallelHookContext`:
353
+
354
+ | Property | Type | Description |
355
+ |----------|------|-------------|
356
+ | `initialArgs` | `any` | The original arguments passed to `handler()`. Single argument stays as-is; multiple arguments become an array. |
357
+
358
+ The final handler receives a `ParallelHookFinalContext`:
359
+
360
+ | Property | Type | Description |
361
+ |----------|------|-------------|
362
+ | `initialArgs` | `any` | Same value passed to every hook. |
363
+ | `results` | `Map<ParallelHookFn, ParallelHookResult>` | One entry per registered hook, keyed by the hook function reference for direct lookup. Iteration order matches registration order. |
364
+
365
+ Each `ParallelHookResult` value is a discriminated union — failures are reported, not thrown:
366
+
367
+ | Field | Type | Description |
368
+ |-------|------|-------------|
369
+ | `status` | `"fulfilled" \| "rejected"` | Discriminator. |
370
+ | `result` | `TResult` | Present when `status === "fulfilled"`. The value the hook returned. Defaults to `any`; tighten via the `ParallelHook<TArgs, TResult>` generic. |
371
+ | `reason` | `unknown` | Present when `status === "rejected"`. The error or value the hook threw. Stays `unknown` regardless of the result generic, since errors in JS aren't typed. |
372
+
373
+ **Basic usage with `Hookified`:**
374
+
375
+ ```javascript
376
+ import { Hookified, ParallelHook } from 'hookified';
377
+
378
+ class MyClass extends Hookified {
379
+ constructor() { super(); }
380
+ }
381
+
382
+ const myClass = new MyClass();
383
+
384
+ const sendEmailHook = async ({ initialArgs }) => sendEmail(initialArgs);
385
+ const sendSlackHook = async ({ initialArgs }) => sendSlack(initialArgs);
386
+ const sendWebhookHook = async ({ initialArgs }) => sendWebhook(initialArgs);
387
+
388
+ const ph = new ParallelHook('notify', ({ results }) => {
389
+ // Look up a specific hook's outcome by reference
390
+ const emailOutcome = results.get(sendEmailHook);
391
+ if (emailOutcome?.status === 'rejected') {
392
+ console.error('email failed:', emailOutcome.reason);
393
+ }
394
+
395
+ // Or iterate every result in registration order
396
+ for (const [hook, r] of results) {
397
+ if (r.status === 'fulfilled') {
398
+ console.log('ok:', r.result);
399
+ } else {
400
+ console.error('failed:', r.reason);
401
+ }
402
+ }
403
+ });
404
+
405
+ ph.addHook(sendEmailHook);
406
+ ph.addHook(sendSlackHook);
407
+ ph.addHook(sendWebhookHook);
408
+
409
+ // Register with Hookified — works because ParallelHook implements IHook
410
+ myClass.onHook(ph);
411
+
412
+ // All three notification hooks fire concurrently, then the final handler runs
413
+ await myClass.hook('notify', { user: 'alice', message: 'hi' });
414
+ ```
415
+
416
+ **Tightening the result type:**
417
+
418
+ When every hook returns the same shape, pass generics so `result` is fully typed instead of `any`:
419
+
420
+ ```typescript
421
+ import { ParallelHook } from 'hookified';
422
+
423
+ type NotifyArgs = { user: string; message: string };
424
+ type NotifyResult = { channel: string; messageId: string };
425
+
426
+ const ph = new ParallelHook<NotifyArgs, NotifyResult>('notify', ({ results }) => {
427
+ for (const [, r] of results) {
428
+ if (r.status === 'fulfilled') {
429
+ console.log(`${r.result.channel}: ${r.result.messageId}`);
430
+ } else {
431
+ console.error(r.reason); // still `unknown` — errors aren't typed
432
+ }
433
+ }
434
+ });
435
+
436
+ ph.addHook(async ({ initialArgs }) => ({ channel: 'email', messageId: '1' }));
437
+ ```
438
+
439
+ **Managing hooks:**
440
+
441
+ ```javascript
442
+ const ph = new ParallelHook('process', ({ results }) => {
443
+ console.log(results.size); // number of hooks that ran
444
+ });
445
+
446
+ const myHook = ({ initialArgs }) => initialArgs + 1;
447
+ ph.addHook(myHook);
448
+
449
+ // Remove a hook by reference
450
+ ph.removeHook(myHook); // returns true
451
+
452
+ // Access the hooks array
453
+ console.log(ph.hooks.length); // 0
454
+ ```
455
+
345
456
  # API - Hooks
346
457
 
347
458
  > All examples below assume the following setup unless otherwise noted:
@@ -1253,6 +1364,22 @@ This shows how on par `hookified` is to the native `EventEmitter` and popular `e
1253
1364
 
1254
1365
  _Note: the `EventEmitter` version is Nodejs versioning._
1255
1366
 
1367
+ # Migrating from v2 to v3
1368
+
1369
+ v3 has no API changes. The only breaking change is the minimum Node.js version requirement.
1370
+
1371
+ ## Breaking Changes
1372
+
1373
+ | Change | Summary |
1374
+ |---|---|
1375
+ | Node.js version | Minimum required version is now `>=22.18.0` (previously no `engines` constraint) |
1376
+
1377
+ ### Node.js >=22.18.0 required
1378
+
1379
+ The `engines` field in `package.json` now requires Node.js 22.18.0 or later. Node.js 20 reached end-of-life in April 2026.
1380
+
1381
+ **Migration:** Upgrade to Node.js 22 LTS or Node.js 24+. No code changes are required — the library API is identical to v2.
1382
+
1256
1383
  # Migrating from v1 to v2
1257
1384
 
1258
1385
  ## Quick Guide