hookified 2.1.1 → 2.2.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
@@ -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!
@@ -31,7 +32,8 @@
31
32
  - [Using it in the Browser](#using-it-in-the-browser)
32
33
  - [Hooks](#hooks)
33
34
  - [Standard Hook](#standard-hook)
34
- - [Waterfall Hook](#waterfallhook)
35
+ - [Waterfall Hook](#waterfall-hook)
36
+ - [Parallel Hook](#parallel-hook)
35
37
  - [API - Hooks](#api---hooks)
36
38
  - [.allowDeprecated](#allowdeprecated)
37
39
  - [.deprecatedHooks](#deprecatedhooks)
@@ -342,6 +344,114 @@ wh.removeHook(myHook); // returns true
342
344
  console.log(wh.hooks.length); // 0
343
345
  ```
344
346
 
347
+ ## Parallel Hook
348
+
349
+ 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()`.
350
+
351
+ Per-hook functions receive a `ParallelHookContext`:
352
+
353
+ | Property | Type | Description |
354
+ |----------|------|-------------|
355
+ | `initialArgs` | `any` | The original arguments passed to `handler()`. Single argument stays as-is; multiple arguments become an array. |
356
+
357
+ The final handler receives a `ParallelHookFinalContext`:
358
+
359
+ | Property | Type | Description |
360
+ |----------|------|-------------|
361
+ | `initialArgs` | `any` | Same value passed to every hook. |
362
+ | `results` | `Map<ParallelHookFn, ParallelHookResult>` | One entry per registered hook, keyed by the hook function reference for direct lookup. Iteration order matches registration order. |
363
+
364
+ Each `ParallelHookResult` value is a discriminated union — failures are reported, not thrown:
365
+
366
+ | Field | Type | Description |
367
+ |-------|------|-------------|
368
+ | `status` | `"fulfilled" \| "rejected"` | Discriminator. |
369
+ | `result` | `TResult` | Present when `status === "fulfilled"`. The value the hook returned. Defaults to `any`; tighten via the `ParallelHook<TArgs, TResult>` generic. |
370
+ | `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. |
371
+
372
+ **Basic usage with `Hookified`:**
373
+
374
+ ```javascript
375
+ import { Hookified, ParallelHook } from 'hookified';
376
+
377
+ class MyClass extends Hookified {
378
+ constructor() { super(); }
379
+ }
380
+
381
+ const myClass = new MyClass();
382
+
383
+ const sendEmailHook = async ({ initialArgs }) => sendEmail(initialArgs);
384
+ const sendSlackHook = async ({ initialArgs }) => sendSlack(initialArgs);
385
+ const sendWebhookHook = async ({ initialArgs }) => sendWebhook(initialArgs);
386
+
387
+ const ph = new ParallelHook('notify', ({ results }) => {
388
+ // Look up a specific hook's outcome by reference
389
+ const emailOutcome = results.get(sendEmailHook);
390
+ if (emailOutcome?.status === 'rejected') {
391
+ console.error('email failed:', emailOutcome.reason);
392
+ }
393
+
394
+ // Or iterate every result in registration order
395
+ for (const [hook, r] of results) {
396
+ if (r.status === 'fulfilled') {
397
+ console.log('ok:', r.result);
398
+ } else {
399
+ console.error('failed:', r.reason);
400
+ }
401
+ }
402
+ });
403
+
404
+ ph.addHook(sendEmailHook);
405
+ ph.addHook(sendSlackHook);
406
+ ph.addHook(sendWebhookHook);
407
+
408
+ // Register with Hookified — works because ParallelHook implements IHook
409
+ myClass.onHook(ph);
410
+
411
+ // All three notification hooks fire concurrently, then the final handler runs
412
+ await myClass.hook('notify', { user: 'alice', message: 'hi' });
413
+ ```
414
+
415
+ **Tightening the result type:**
416
+
417
+ When every hook returns the same shape, pass generics so `result` is fully typed instead of `any`:
418
+
419
+ ```typescript
420
+ import { ParallelHook } from 'hookified';
421
+
422
+ type NotifyArgs = { user: string; message: string };
423
+ type NotifyResult = { channel: string; messageId: string };
424
+
425
+ const ph = new ParallelHook<NotifyArgs, NotifyResult>('notify', ({ results }) => {
426
+ for (const [, r] of results) {
427
+ if (r.status === 'fulfilled') {
428
+ console.log(`${r.result.channel}: ${r.result.messageId}`);
429
+ } else {
430
+ console.error(r.reason); // still `unknown` — errors aren't typed
431
+ }
432
+ }
433
+ });
434
+
435
+ ph.addHook(async ({ initialArgs }) => ({ channel: 'email', messageId: '1' }));
436
+ ```
437
+
438
+ **Managing hooks:**
439
+
440
+ ```javascript
441
+ const ph = new ParallelHook('process', ({ results }) => {
442
+ console.log(results.size); // number of hooks that ran
443
+ });
444
+
445
+ const myHook = ({ initialArgs }) => initialArgs + 1;
446
+ ph.addHook(myHook);
447
+
448
+ // Remove a hook by reference
449
+ ph.removeHook(myHook); // returns true
450
+
451
+ // Access the hooks array
452
+ console.log(ph.hooks.length); // 0
453
+ ```
454
+
345
455
  # API - Hooks
346
456
 
347
457
  > All examples below assume the following setup unless otherwise noted: