hookified 1.15.0 → 2.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
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![tests](https://github.com/jaredwray/hookified/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/hookified/actions/workflows/tests.yaml)
6
6
  [![GitHub license](https://img.shields.io/github/license/jaredwray/hookified)](https://github.com/jaredwray/hookified/blob/master/LICENSE)
7
- [![codecov](https://codecov.io/gh/jaredwray/hookified/graph/badge.svg?token=nKkVklTFdA)](https://codecov.io/gh/jaredwray/hookified)
7
+ [![codecov](https://codecov.io/gh/jaredwray/hookified/branch/main/graph/badge.svg?token=nKkVklTFdA)](https://codecov.io/gh/jaredwray/hookified)
8
8
  [![npm](https://img.shields.io/npm/dm/hookified)](https://npmjs.com/package/hookified)
9
9
  [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/hookified/badge)](https://www.jsdelivr.com/package/npm/hookified)
10
10
  [![npm](https://img.shields.io/npm/v/hookified)](https://npmjs.com/package/hookified)
@@ -19,37 +19,45 @@
19
19
  - Enforce consistent hook naming conventions with `enforceBeforeAfter`
20
20
  - Deprecation warnings for hooks with `deprecatedHooks`
21
21
  - Control deprecated hook execution with `allowDeprecated`
22
- - No package dependencies and only 200KB in size
22
+ - WaterfallHook for sequential data transformation pipelines
23
+ - No package dependencies and only 250KB in size
23
24
  - Fast and Efficient with [Benchmarks](#benchmarks)
24
25
  - Maintained on a regular basis!
25
26
 
26
27
  # Table of Contents
27
28
  - [Installation](#installation)
28
29
  - [Usage](#usage)
30
+ - [Migrating from v1 to v2](#migrating-from-v1-to-v2)
29
31
  - [Using it in the Browser](#using-it-in-the-browser)
32
+ - [Hooks](#hooks)
33
+ - [Standard Hook](#standard-hook)
34
+ - [Waterfall Hook](#waterfallhook)
30
35
  - [API - Hooks](#api---hooks)
31
- - [.throwOnHookError](#throwhookerror)
32
- - [.logger](#logger)
33
- - [.enforceBeforeAfter](#enforcebeforeafter)
34
- - [.deprecatedHooks](#deprecatedhooks)
35
36
  - [.allowDeprecated](#allowdeprecated)
36
- - [.onHook(eventName, handler)](#onhookeventname-handler)
37
- - [.onHookEntry(hookEntry)](#onhookentryhookentry)
38
- - [.addHook(eventName, handler)](#addhookeventname-handler)
39
- - [.onHooks(Array)](#onhooksarray)
40
- - [.onceHook(eventName, handler)](#oncehookeventname-handler)
41
- - [.prependHook(eventName, handler)](#prependhookeventname-handler)
42
- - [.prependOnceHook(eventName, handler)](#prependoncehookeventname-handler)
43
- - [.removeHook(eventName)](#removehookeventname)
44
- - [.removeHooks(Array)](#removehooksarray)
45
- - [.hook(eventName, ...args)](#hookeventname-args)
46
- - [.callHook(eventName, ...args)](#callhookeventname-args)
47
- - [.beforeHook(eventName, ...args)](#beforehookeventname-args)
37
+ - [.deprecatedHooks](#deprecatedhooks)
38
+ - [.enforceBeforeAfter](#enforcebeforeafter)
39
+ - [.eventLogger](#eventlogger)
40
+ - [.hooks](#hooks-1)
41
+ - [.throwOnHookError](#throwOnHookError)
42
+ - [.useHookClone](#usehookclone)
43
+ - [.addHook(event, handler)](#addhookevent-handler)
48
44
  - [.afterHook(eventName, ...args)](#afterhookeventname-args)
49
- - [.hookSync(eventName, ...args)](#hooksync-eventname-args)
50
- - [.hooks](#hooks)
45
+ - [.beforeHook(eventName, ...args)](#beforehookeventname-args)
46
+ - [.callHook(eventName, ...args)](#callhookeventname-args)
47
+ - [.clearHooks()](#clearhooks)
48
+ - [.getHook(id)](#gethookid)
51
49
  - [.getHooks(eventName)](#gethookseventname)
52
- - [.clearHooks(eventName)](#clearhookeventname)
50
+ - [.hook(eventName, ...args)](#hookeventname-args)
51
+ - [.hookSync(eventName, ...args)](#hooksync-eventname-args)
52
+ - [.onHook(hook, options?)](#onhookhook-options)
53
+ - [.onHooks(Array, options?)](#onhooksarray-options)
54
+ - [.onceHook(hook)](#oncehookhook)
55
+ - [.prependHook(hook, options?)](#prependhookhook-options)
56
+ - [.prependOnceHook(hook, options?)](#prependoncehookhook-options)
57
+ - [.removeEventHooks(eventName)](#removeeventhookseventname)
58
+ - [.removeHook(hook)](#removehookhook)
59
+ - [.removeHookById(id)](#removehookbyidid)
60
+ - [.removeHooks(Array)](#removehooksarray)
53
61
  - [API - Events](#api---events)
54
62
  - [.throwOnEmitError](#throwonemitterror)
55
63
  - [.throwOnEmptyListeners](#throwonemptylisteners)
@@ -87,11 +95,11 @@ class MyClass extends Hookified {
87
95
  }
88
96
 
89
97
  async myMethodEmittingEvent() {
90
- this.emit('message', 'Hello World'); //using Emittery
98
+ this.emit('message', 'Hello World');
91
99
  }
92
100
 
93
101
  //with hooks you can pass data in and if they are subscribed via onHook they can modify the data
94
- async myMethodWithHooks() Promise<any> {
102
+ async myMethodWithHooks(): Promise<any> {
95
103
  let data = { some: 'data' };
96
104
  // do something
97
105
  await this.hook('before:myMethod2', data);
@@ -111,7 +119,7 @@ class MyClass extends Hookified {
111
119
  super();
112
120
  }
113
121
 
114
- async myMethodWithHooks() Promise<any> {
122
+ async myMethodWithHooks(): Promise<any> {
115
123
  let data = { some: 'data' };
116
124
  let data2 = { some: 'data2' };
117
125
  // do something
@@ -134,11 +142,11 @@ class MyClass extends Hookified {
134
142
  }
135
143
 
136
144
  async myMethodEmittingEvent() {
137
- this.emit('message', 'Hello World'); //using Emittery
145
+ this.emit('message', 'Hello World');
138
146
  }
139
147
 
140
148
  //with hooks you can pass data in and if they are subscribed via onHook they can modify the data
141
- async myMethodWithHooks() Promise<any> {
149
+ async myMethodWithHooks(): Promise<any> {
142
150
  let data = { some: 'data' };
143
151
  // do something
144
152
  await this.hook('before:myMethod2', data);
@@ -160,11 +168,11 @@ if you are not using ESM modules, you can use the following:
160
168
  }
161
169
 
162
170
  async myMethodEmittingEvent() {
163
- this.emit('message', 'Hello World'); //using Emittery
171
+ this.emit('message', 'Hello World');
164
172
  }
165
173
 
166
174
  //with hooks you can pass data in and if they are subscribed via onHook they can modify the data
167
- async myMethodWithHooks() Promise<any> {
175
+ async myMethodWithHooks(): Promise<any> {
168
176
  let data = { some: 'data' };
169
177
  // do something
170
178
  await this.hook('before:myMethod2', data);
@@ -175,191 +183,175 @@ if you are not using ESM modules, you can use the following:
175
183
  </script>
176
184
  ```
177
185
 
178
- # API - Hooks
186
+ # Hooks
179
187
 
180
- ## .throwOnHookError
188
+ ## Standard Hook
181
189
 
182
- If set to true, errors thrown in hooks will be thrown. If set to false, errors will be only emitted.
190
+ The `Hook` class provides a convenient way to create hook entries. It implements the `IHook` interface.
191
+
192
+ The `IHook` interface has the following properties:
193
+
194
+ | Property | Type | Required | Description |
195
+ |----------|------|----------|-------------|
196
+ | `id` | `string` | No | Unique identifier for the hook. Auto-generated via `crypto.randomUUID()` if not provided. |
197
+ | `event` | `string` | Yes | The event name for the hook. |
198
+ | `handler` | `HookFn` | Yes | The handler function for the hook. |
199
+
200
+ When a hook is registered, it is assigned an `id` (auto-generated if not provided). The `id` can be used to look up or remove hooks via `getHook` and `removeHookById`. If you register a hook with the same `id` on the same event, it will replace the existing hook in-place (preserving its position).
201
+
202
+ **Using the `Hook` class:**
183
203
 
184
204
  ```javascript
185
- import { Hookified } from 'hookified';
205
+ import { Hook, Hookified } from 'hookified';
186
206
 
187
207
  class MyClass extends Hookified {
188
- constructor() {
189
- super({ throwOnHookError: true });
190
- }
208
+ constructor() { super(); }
191
209
  }
192
210
 
193
211
  const myClass = new MyClass();
194
212
 
195
- console.log(myClass.throwOnHookError); // true. because it is set in super
213
+ // Without id (auto-generated)
214
+ const hook = new Hook('before:save', async (data) => {
215
+ data.validated = true;
216
+ });
196
217
 
197
- try {
198
- myClass.onHook('error-event', async () => {
199
- throw new Error('error');
200
- });
218
+ // With id
219
+ const hook2 = new Hook('after:save', async (data) => {
220
+ console.log('saved');
221
+ }, 'my-after-save-hook');
201
222
 
202
- await myClass.hook('error-event');
203
- } catch (error) {
204
- console.log(error.message); // error
205
- }
223
+ // Register with onHook
224
+ myClass.onHook(hook);
206
225
 
207
- myClass.throwOnHookError = false;
208
- console.log(myClass.throwOnHookError); // false
209
- ```
226
+ // Or register multiple hooks with onHooks
227
+ const hooks = [
228
+ new Hook('before:save', async (data) => { data.validated = true; }),
229
+ new Hook('after:save', async (data) => { console.log('saved'); }),
230
+ ];
231
+ myClass.onHooks(hooks);
210
232
 
211
- ## .logger
212
- If set, errors thrown in hooks will be logged to the logger. If not set, errors will be only emitted.
233
+ // Remove hooks
234
+ myClass.removeHooks(hooks);
235
+ ```
213
236
 
214
- ```javascript
215
- import { Hookified } from 'hookified';
216
- import pino from 'pino';
237
+ **Using plain TypeScript with the `IHook` interface:**
217
238
 
218
- const logger = pino(); // create a logger instance that is compatible with Logger type
239
+ ```typescript
240
+ import { Hookified, type IHook } from 'hookified';
219
241
 
220
242
  class MyClass extends Hookified {
221
- constructor() {
222
- super({ logger });
223
- }
224
-
225
- async myMethodWithHooks() Promise<any> {
226
- let data = { some: 'data' };
227
- // do something
228
- await this.hook('before:myMethod2', data);
229
-
230
- return data;
231
- }
243
+ constructor() { super(); }
232
244
  }
233
245
 
234
246
  const myClass = new MyClass();
235
- myClass.onHook('before:myMethod2', async () => {
236
- throw new Error('error');
237
- });
238
247
 
239
- // when you call before:myMethod2 it will log the error to the logger
240
- await myClass.hook('before:myMethod2');
248
+ const hook: IHook = {
249
+ id: 'my-validation-hook', // optional — auto-generated if omitted
250
+ event: 'before:save',
251
+ handler: async (data) => {
252
+ data.validated = true;
253
+ },
254
+ };
255
+
256
+ const stored = myClass.onHook(hook);
257
+ console.log(stored?.id); // 'my-validation-hook'
258
+
259
+ // Later, remove by id
260
+ myClass.removeHookById('my-validation-hook');
241
261
  ```
242
262
 
243
- ## .enforceBeforeAfter
263
+ ## Waterfall Hook
244
264
 
245
- If set to true, enforces that all hook names must start with 'before' or 'after'. This is useful for maintaining consistent hook naming conventions in your application. Default is false.
265
+ The `WaterfallHook` class chains multiple hook functions sequentially in a waterfall pipeline. Each hook receives a context containing the original arguments and the accumulated results from all previous hooks. It implements the `IHook` interface, so it integrates directly with `Hookified.onHook()`.
246
266
 
247
- ```javascript
248
- import { Hookified } from 'hookified';
267
+ The `WaterfallHookContext` has the following properties:
249
268
 
250
- class MyClass extends Hookified {
251
- constructor() {
252
- super({ enforceBeforeAfter: true });
253
- }
254
- }
269
+ | Property | Type | Description |
270
+ |----------|------|-------------|
271
+ | `initialArgs` | `any` | The original arguments passed to the waterfall execution. |
272
+ | `results` | `WaterfallHookResult[]` | Array of `{ hook, result }` entries from previous hooks. Empty for the first hook. |
255
273
 
256
- const myClass = new MyClass();
274
+ **Basic usage:**
257
275
 
258
- console.log(myClass.enforceBeforeAfter); // true
276
+ ```javascript
277
+ import { WaterfallHook } from 'hookified';
259
278
 
260
- // These will work fine
261
- myClass.onHook('beforeSave', async () => {
262
- console.log('Before save hook');
279
+ const wh = new WaterfallHook('process', ({ results, initialArgs }) => {
280
+ // Final handler receives all accumulated results
281
+ const lastResult = results[results.length - 1].result;
282
+ console.log('Final:', lastResult);
263
283
  });
264
284
 
265
- myClass.onHook('afterSave', async () => {
266
- console.log('After save hook');
285
+ // Add transformation hooks to the pipeline
286
+ wh.addHook(({ initialArgs }) => {
287
+ return initialArgs + 1; // 5 -> 6
267
288
  });
268
289
 
269
- myClass.onHook('before:validation', async () => {
270
- console.log('Before validation hook');
290
+ wh.addHook(({ results }) => {
291
+ return results[results.length - 1].result * 2; // 6 -> 12
271
292
  });
272
293
 
273
- // This will throw an error
274
- try {
275
- myClass.onHook('customEvent', async () => {
276
- console.log('This will not work');
277
- });
278
- } catch (error) {
279
- console.log(error.message); // Hook event "customEvent" must start with "before" or "after" when enforceBeforeAfter is enabled
280
- }
281
-
282
- // You can also change it dynamically
283
- myClass.enforceBeforeAfter = false;
284
- myClass.onHook('customEvent', async () => {
285
- console.log('This will work now');
286
- });
294
+ // Execute the waterfall by calling handler directly
295
+ await wh.handler(5); // Final: 12
287
296
  ```
288
297
 
289
- The validation applies to all hook-related methods:
290
- - `onHook()`, `addHook()`, `onHookEntry()`, `onHooks()`
291
- - `prependHook()`, `onceHook()`, `prependOnceHook()`
292
- - `hook()`, `callHook()`
293
- - `getHooks()`, `removeHook()`, `removeHooks()`
294
-
295
- Note: The `beforeHook()` and `afterHook()` helper methods automatically generate proper hook names and work regardless of the `enforceBeforeAfter` setting.
296
-
297
- ## .deprecatedHooks
298
-
299
- A Map of deprecated hook names to deprecation messages. When a deprecated hook is used, a warning will be emitted via the 'warn' event and logged to the logger (if available). Default is an empty Map.
298
+ **Integrating with Hookified via `onHook()`:**
300
299
 
301
300
  ```javascript
302
- import { Hookified } from 'hookified';
303
-
304
- // Define deprecated hooks with custom messages
305
- const deprecatedHooks = new Map([
306
- ['oldHook', 'Use newHook instead'],
307
- ['legacyMethod', 'This hook will be removed in v2.0'],
308
- ['deprecatedFeature', ''] // Empty message - will just say "deprecated"
309
- ]);
301
+ import { Hookified, WaterfallHook } from 'hookified';
310
302
 
311
303
  class MyClass extends Hookified {
312
- constructor() {
313
- super({ deprecatedHooks });
314
- }
304
+ constructor() { super(); }
315
305
  }
316
306
 
317
307
  const myClass = new MyClass();
318
308
 
319
- console.log(myClass.deprecatedHooks); // Map with deprecated hooks
320
-
321
- // Listen for deprecation warnings
322
- myClass.on('warn', (event) => {
323
- console.log(`Deprecation warning: ${event.message}`);
324
- // event.hook contains the hook name
325
- // event.message contains the full warning message
309
+ const wh = new WaterfallHook('save', ({ results }) => {
310
+ const data = results[results.length - 1].result;
311
+ console.log('Saved:', data);
326
312
  });
327
313
 
328
- // Using a deprecated hook will emit warnings
329
- myClass.onHook('oldHook', () => {
330
- console.log('This hook is deprecated');
314
+ wh.addHook(({ initialArgs }) => {
315
+ return { ...initialArgs, validated: true };
331
316
  });
332
- // Output: Hook "oldHook" is deprecated: Use newHook instead
333
317
 
334
- // Using a deprecated hook with empty message
335
- myClass.onHook('deprecatedFeature', () => {
336
- console.log('This hook is deprecated');
318
+ wh.addHook(({ results }) => {
319
+ return { ...results[results.length - 1].result, timestamp: Date.now() };
337
320
  });
338
- // Output: Hook "deprecatedFeature" is deprecated
339
321
 
340
- // You can also set deprecated hooks dynamically
341
- myClass.deprecatedHooks.set('anotherOldHook', 'Please migrate to the new API');
322
+ // Register with Hookified works because WaterfallHook implements IHook
323
+ myClass.onHook(wh);
342
324
 
343
- // Works with logger if provided
344
- import pino from 'pino';
345
- const logger = pino();
325
+ // When hook() fires, the full waterfall pipeline executes
326
+ await myClass.hook('save', { name: 'test' });
327
+ // Saved: { name: 'test', validated: true, timestamp: ... }
328
+ ```
346
329
 
347
- const myClassWithLogger = new Hookified({
348
- deprecatedHooks,
349
- logger
350
- });
330
+ **Managing hooks:**
351
331
 
352
- // Deprecation warnings will be logged to logger.warn
332
+ ```javascript
333
+ const wh = new WaterfallHook('process', ({ results }) => results);
334
+
335
+ const myHook = ({ initialArgs }) => initialArgs + 1;
336
+ wh.addHook(myHook);
337
+
338
+ // Remove a hook by reference
339
+ wh.removeHook(myHook); // returns true
340
+
341
+ // Access the hooks array
342
+ console.log(wh.hooks.length); // 0
353
343
  ```
354
344
 
355
- The deprecation warning system applies to all hook-related methods:
356
- - Registration: `onHook()`, `addHook()`, `onHookEntry()`, `onHooks()`, `prependHook()`, `onceHook()`, `prependOnceHook()`
357
- - Execution: `hook()`, `callHook()`
358
- - Management: `getHooks()`, `removeHook()`, `removeHooks()`
345
+ # API - Hooks
359
346
 
360
- Deprecation warnings are emitted in two ways:
361
- 1. **Event**: A 'warn' event is emitted with `{ hook: string, message: string }`
362
- 2. **Logger**: Logged to `logger.warn()` if a logger is configured and has a `warn` method
347
+ > All examples below assume the following setup unless otherwise noted:
348
+ > ```javascript
349
+ > import { Hookified } from 'hookified';
350
+ > class MyClass extends Hookified {
351
+ > constructor(options) { super(options); }
352
+ > }
353
+ > const myClass = new MyClass();
354
+ > ```
363
355
 
364
356
  ## .allowDeprecated
365
357
 
@@ -388,9 +380,9 @@ myClass.on('warn', (event) => {
388
380
  });
389
381
 
390
382
  // Try to register a deprecated hook - will emit warning but not register
391
- myClass.onHook('oldHook', () => {
383
+ myClass.onHook({ event: 'oldHook', handler: () => {
392
384
  console.log('This will never execute');
393
- });
385
+ }});
394
386
  // Output: Warning: Hook "oldHook" is deprecated: Use newHook instead
395
387
 
396
388
  // Verify hook was not registered
@@ -402,9 +394,9 @@ await myClass.hook('oldHook');
402
394
  // (but no handlers execute)
403
395
 
404
396
  // Non-deprecated hooks work normally
405
- myClass.onHook('validHook', () => {
397
+ myClass.onHook({ event: 'validHook', handler: () => {
406
398
  console.log('This works fine');
407
- });
399
+ }});
408
400
 
409
401
  console.log(myClass.getHooks('validHook')); // [handler function]
410
402
 
@@ -412,17 +404,17 @@ console.log(myClass.getHooks('validHook')); // [handler function]
412
404
  myClass.allowDeprecated = true;
413
405
 
414
406
  // Now deprecated hooks can be registered and executed
415
- myClass.onHook('oldHook', () => {
407
+ myClass.onHook({ event: 'oldHook', handler: () => {
416
408
  console.log('Now this works');
417
- });
409
+ }});
418
410
 
419
411
  console.log(myClass.getHooks('oldHook')); // [handler function]
420
412
  ```
421
413
 
422
414
  **Behavior when `allowDeprecated` is false:**
423
415
  - **Registration**: All hook registration methods (`onHook`, `addHook`, `prependHook`, etc.) will emit warnings but skip registration
424
- - **Execution**: Hook execution methods (`hook`, `callHook`) will emit warnings but skip execution
425
- - **Management**: Hook management methods (`getHooks`, `removeHook`) will emit warnings and return undefined/skip operations
416
+ - **Execution**: Hook execution methods (`hook`, `callHook`) will emit warnings but skip execution
417
+ - **Removal/Reading**: `removeHook`, `removeHooks`, and `getHooks` always work regardless of deprecation status
426
418
  - **Warnings**: Deprecation warnings are always emitted regardless of `allowDeprecated` setting
427
419
 
428
420
  **Use cases:**
@@ -431,543 +423,550 @@ console.log(myClass.getHooks('oldHook')); // [handler function]
431
423
  - **Migration**: Gradually disable deprecated hooks during API transitions
432
424
  - **Production**: Disable deprecated hooks to prevent legacy code execution
433
425
 
434
- ## .onHook(eventName, handler)
426
+ ## .deprecatedHooks
435
427
 
436
- Subscribe to a hook event.
428
+ A Map of deprecated hook names to deprecation messages. When a deprecated hook is used, a warning will be emitted via the 'warn' event and logged to the logger (if available). Default is an empty Map.
437
429
 
438
430
  ```javascript
439
431
  import { Hookified } from 'hookified';
440
432
 
433
+ // Define deprecated hooks with custom messages
434
+ const deprecatedHooks = new Map([
435
+ ['oldHook', 'Use newHook instead'],
436
+ ['legacyMethod', 'This hook will be removed in v2.0'],
437
+ ['deprecatedFeature', ''] // Empty message - will just say "deprecated"
438
+ ]);
439
+
441
440
  class MyClass extends Hookified {
442
441
  constructor() {
443
- super();
444
- }
445
-
446
- async myMethodWithHooks() Promise<any> {
447
- let data = { some: 'data' };
448
- // do something
449
- await this.hook('before:myMethod2', data);
450
-
451
- return data;
442
+ super({ deprecatedHooks });
452
443
  }
453
444
  }
454
445
 
455
446
  const myClass = new MyClass();
456
- myClass.onHook('before:myMethod2', async (data) => {
457
- data.some = 'new data';
458
- });
459
- ```
460
447
 
461
- ## .onHookEntry(hookEntry)
448
+ console.log(myClass.deprecatedHooks); // Map with deprecated hooks
462
449
 
463
- This allows you to create a hook with the `HookEntry` type which includes the event and handler. This is useful for creating hooks with a single object.
450
+ // Listen for deprecation warnings
451
+ myClass.on('warn', (event) => {
452
+ console.log(`Deprecation warning: ${event.message}`);
453
+ // event.hook contains the hook name
454
+ // event.message contains the full warning message
455
+ });
464
456
 
465
- ```javascript
466
- import { Hookified, HookEntry } from 'hookified';
457
+ // Using a deprecated hook will emit warnings
458
+ myClass.onHook({ event: 'oldHook', handler: () => {
459
+ console.log('This hook is deprecated');
460
+ }});
461
+ // Output: Hook "oldHook" is deprecated: Use newHook instead
467
462
 
468
- class MyClass extends Hookified {
469
- constructor() {
470
- super();
471
- }
463
+ // Using a deprecated hook with empty message
464
+ myClass.onHook({ event: 'deprecatedFeature', handler: () => {
465
+ console.log('This hook is deprecated');
466
+ }});
467
+ // Output: Hook "deprecatedFeature" is deprecated
472
468
 
473
- async myMethodWithHooks() Promise<any> {
474
- let data = { some: 'data' };
475
- // do something
476
- await this.hook('before:myMethod2', data);
469
+ // You can also set deprecated hooks dynamically
470
+ myClass.deprecatedHooks.set('anotherOldHook', 'Please migrate to the new API');
477
471
 
478
- return data;
479
- }
480
- }
472
+ // Works with logger if provided
473
+ import pino from 'pino';
474
+ const logger = pino();
481
475
 
482
- const myClass = new MyClass();
483
- myClass.onHookEntry({
484
- event: 'before:myMethod2',
485
- handler: async (data) => {
486
- data.some = 'new data';
487
- },
476
+ const myClassWithLogger = new Hookified({
477
+ deprecatedHooks,
478
+ eventLogger: logger
488
479
  });
480
+
481
+ // Deprecation warnings will be logged to logger.warn
489
482
  ```
490
483
 
491
- ## .addHook(eventName, handler)
484
+ The deprecation warning system applies to the following hook-related methods:
485
+ - Registration: `onHook()`, `addHook()`, `onHooks()`, `prependHook()`, `onceHook()`, `prependOnceHook()`
486
+ - Execution: `hook()`, `callHook()`
487
+
488
+ Note: `getHooks()`, `removeHook()`, and `removeHooks()` do not check for deprecated hooks and always operate normally.
492
489
 
493
- This is an alias for `.onHook(eventName, handler)` for backwards compatibility.
490
+ Deprecation warnings are emitted in two ways:
491
+ 1. **Event**: A 'warn' event is emitted with `{ hook: string, message: string }`
492
+ 2. **Logger**: Logged to `eventLogger.warn()` if an `eventLogger` is configured and has a `warn` method
494
493
 
495
- ## .onHooks(Array)
494
+ ## .enforceBeforeAfter
496
495
 
497
- Subscribe to multiple hook events at once
496
+ If set to true, enforces that all hook names must start with 'before' or 'after'. This is useful for maintaining consistent hook naming conventions in your application. Default is false.
498
497
 
499
498
  ```javascript
500
499
  import { Hookified } from 'hookified';
501
500
 
502
501
  class MyClass extends Hookified {
503
502
  constructor() {
504
- super();
503
+ super({ enforceBeforeAfter: true });
505
504
  }
505
+ }
506
506
 
507
- async myMethodWithHooks() Promise<any> {
508
- let data = { some: 'data' };
509
- await this.hook('before:myMethodWithHooks', data);
510
-
511
- // do something here with the data
512
- data.some = 'new data';
507
+ const myClass = new MyClass();
513
508
 
514
- await this.hook('after:myMethodWithHooks', data);
509
+ console.log(myClass.enforceBeforeAfter); // true
515
510
 
516
- return data;
517
- }
511
+ // These will work fine
512
+ myClass.onHook({ event: 'beforeSave', handler: async () => {
513
+ console.log('Before save hook');
514
+ }});
515
+
516
+ myClass.onHook({ event: 'afterSave', handler: async () => {
517
+ console.log('After save hook');
518
+ }});
519
+
520
+ myClass.onHook({ event: 'before:validation', handler: async () => {
521
+ console.log('Before validation hook');
522
+ }});
523
+
524
+ // This will throw an error
525
+ try {
526
+ myClass.onHook({ event: 'customEvent', handler: async () => {
527
+ console.log('This will not work');
528
+ }});
529
+ } catch (error) {
530
+ console.log(error.message); // Hook event "customEvent" must start with "before" or "after" when enforceBeforeAfter is enabled
518
531
  }
519
532
 
520
- const myClass = new MyClass();
521
- const hooks = [
522
- {
523
- event: 'before:myMethodWithHooks',
524
- handler: async (data) => {
525
- data.some = 'new data1';
526
- },
527
- },
528
- {
529
- event: 'after:myMethodWithHooks',
530
- handler: async (data) => {
531
- data.some = 'new data2';
532
- },
533
- },
534
- ];
533
+ // You can also change it dynamically
534
+ myClass.enforceBeforeAfter = false;
535
+ myClass.onHook({ event: 'customEvent', handler: async () => {
536
+ console.log('This will work now');
537
+ }});
535
538
  ```
536
539
 
537
- ## .onceHook(eventName, handler)
540
+ The validation applies to all hook-related methods:
541
+ - `onHook()`, `addHook()`, `onHooks()`
542
+ - `prependHook()`, `onceHook()`, `prependOnceHook()`
543
+ - `hook()`, `callHook()`
544
+ - `getHooks()`, `removeHook()`, `removeHooks()`
545
+
546
+ Note: The `beforeHook()` and `afterHook()` helper methods automatically generate proper hook names and work regardless of the `enforceBeforeAfter` setting.
538
547
 
539
- Subscribe to a hook event once.
548
+ ## .eventLogger
549
+ If set, errors thrown in hooks will be logged to the logger. If not set, errors will be only emitted.
540
550
 
541
551
  ```javascript
542
- import { Hookified } from 'hookified';
552
+ import pino from 'pino';
543
553
 
544
- class MyClass extends Hookified {
545
- constructor() {
546
- super();
547
- }
554
+ const myClass = new MyClass({ eventLogger: pino() });
548
555
 
549
- async myMethodWithHooks() Promise<any> {
550
- let data = { some: 'data' };
551
- // do something
552
- await this.hook('before:myMethod2', data);
556
+ myClass.onHook({ event: 'before:myMethod2', handler: async () => {
557
+ throw new Error('error');
558
+ }});
553
559
 
554
- return data;
555
- }
556
- }
560
+ // when you call before:myMethod2 it will log the error to the logger
561
+ await myClass.hook('before:myMethod2');
562
+ ```
557
563
 
558
- const myClass = new MyClass();
564
+ ## .hooks
559
565
 
560
- myClass.onHookOnce('before:myMethod2', async (data) => {
561
- data.some = 'new data';
562
- });
566
+ Get all hooks. Returns a `Map<string, IHook[]>` where each key is an event name and the value is an array of `IHook` objects.
563
567
 
564
- myClass.myMethodWithHooks();
568
+ ```javascript
569
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
570
+ data.some = 'new data';
571
+ }});
565
572
 
566
- console.log(myClass.hooks.length); // 0
573
+ console.log(myClass.hooks); // Map { 'before:myMethod2' => [{ event: 'before:myMethod2', handler: [Function] }] }
567
574
  ```
568
575
 
569
- ## .prependHook(eventName, handler)
576
+ ## .throwOnHookError
570
577
 
571
- Subscribe to a hook event before all other hooks.
578
+ If set to true, errors thrown in hooks will be thrown. If set to false, errors will be only emitted.
572
579
 
573
580
  ```javascript
574
- import { Hookified } from 'hookified';
581
+ const myClass = new MyClass({ throwOnHookError: true });
575
582
 
576
- class MyClass extends Hookified {
577
- constructor() {
578
- super();
579
- }
583
+ console.log(myClass.throwOnHookError); // true
580
584
 
581
- async myMethodWithHooks() Promise<any> {
582
- let data = { some: 'data' };
583
- // do something
584
- await this.hook('before:myMethod2', data);
585
+ try {
586
+ myClass.onHook({ event: 'error-event', handler: async () => {
587
+ throw new Error('error');
588
+ }});
585
589
 
586
- return data;
587
- }
590
+ await myClass.hook('error-event');
591
+ } catch (error) {
592
+ console.log(error.message); // error
588
593
  }
589
594
 
590
- const myClass = new MyClass();
591
- myClass.onHook('before:myMethod2', async (data) => {
592
- data.some = 'new data';
593
- });
594
- myClass.preHook('before:myMethod2', async (data) => {
595
- data.some = 'will run before new data';
596
- });
595
+ myClass.throwOnHookError = false;
596
+ console.log(myClass.throwOnHookError); // false
597
597
  ```
598
598
 
599
- ## .prependOnceHook(eventName, handler)
599
+ ## .useHookClone
600
600
 
601
- Subscribe to a hook event before all other hooks. After it is used once it will be removed.
601
+ Controls whether hook objects are cloned before storing internally. Default is `true`. When `true`, a shallow copy of the `IHook` object is stored, preventing external mutation from affecting registered hooks. When `false`, the original reference is stored directly.
602
602
 
603
603
  ```javascript
604
- import { Hookified } from 'hookified';
604
+ const myClass = new MyClass({ useHookClone: false });
605
605
 
606
- class MyClass extends Hookified {
607
- constructor() {
608
- super();
609
- }
606
+ const hook = { event: 'before:save', handler: async (data) => {} };
607
+ myClass.onHook(hook);
610
608
 
611
- async myMethodWithHooks() Promise<any> {
612
- let data = { some: 'data' };
613
- // do something
614
- await this.hook('before:myMethod2', data);
609
+ // With useHookClone: false, the stored hook is the same reference
610
+ const storedHooks = myClass.getHooks('before:save');
611
+ console.log(storedHooks[0] === hook); // true
615
612
 
616
- return data;
617
- }
618
- }
613
+ // You can dynamically change the setting
614
+ myClass.useHookClone = true;
615
+ ```
619
616
 
620
- const myClass = new MyClass();
621
- myClass.onHook('before:myMethod2', async (data) => {
617
+ ## .addHook(event, handler)
618
+
619
+ This is an alias for `.onHook()` that takes an event name and handler function directly.
620
+
621
+ ```javascript
622
+ myClass.addHook('before:myMethod2', async (data) => {
622
623
  data.some = 'new data';
623
624
  });
624
- myClass.preHook('before:myMethod2', async (data) => {
625
- data.some = 'will run before new data';
626
- });
627
625
  ```
628
626
 
629
- ## .removeHook(eventName)
627
+ ## .afterHook(eventName, ...args)
630
628
 
631
- Unsubscribe from a hook event.
629
+ This is a helper function that will prepend a hook name with `after:`.
632
630
 
633
631
  ```javascript
634
- import { Hookified } from 'hookified';
632
+ // Inside your class method — the event name will be `after:myMethod2`
633
+ await this.afterHook('myMethod2', data);
634
+ ```
635
635
 
636
- class MyClass extends Hookified {
637
- constructor() {
638
- super();
639
- }
636
+ ## .beforeHook(eventName, ...args)
640
637
 
641
- async myMethodWithHooks() Promise<any> {
642
- let data = { some: 'data' };
643
- // do something
644
- await this.hook('before:myMethod2', data);
638
+ This is a helper function that will prepend a hook name with `before:`.
645
639
 
646
- return data;
647
- }
648
- }
640
+ ```javascript
641
+ // Inside your class method — the event name will be `before:myMethod2`
642
+ await this.beforeHook('myMethod2', data);
643
+ ```
649
644
 
650
- const myClass = new MyClass();
651
- const handler = async (data) => {
652
- data.some = 'new data';
653
- };
645
+ ## .callHook(eventName, ...args)
654
646
 
655
- myClass.onHook('before:myMethod2', handler);
647
+ This is an alias for `.hook(eventName, ...args)` for backwards compatibility.
656
648
 
657
- myClass.removeHook('before:myMethod2', handler);
658
- ```
649
+ ## .clearHooks()
659
650
 
660
- ## .removeHooks(Array)
661
- Unsubscribe from multiple hooks.
651
+ Clear all hooks across all events.
662
652
 
663
653
  ```javascript
664
- import { Hookified } from 'hookified';
654
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
655
+ data.some = 'new data';
656
+ }});
665
657
 
666
- class MyClass extends Hookified {
667
- constructor() {
668
- super();
669
- }
658
+ myClass.clearHooks();
659
+ ```
670
660
 
671
- async myMethodWithHooks() Promise<any> {
672
- let data = { some: 'data' };
673
- await this.hook('before:myMethodWithHooks', data);
674
-
675
- // do something
676
- data.some = 'new data';
677
- await this.hook('after:myMethodWithHooks', data);
661
+ ## .getHook(id)
678
662
 
679
- return data;
680
- }
681
- }
663
+ Get a specific hook by `id`, searching across all events. Returns the `IHook` if found, or `undefined`.
682
664
 
665
+ ```javascript
683
666
  const myClass = new MyClass();
684
667
 
685
- const hooks = [
686
- {
687
- event: 'before:myMethodWithHooks',
688
- handler: async (data) => {
689
- data.some = 'new data1';
690
- },
691
- },
692
- {
693
- event: 'after:myMethodWithHooks',
694
- handler: async (data) => {
695
- data.some = 'new data2';
696
- },
697
- },
698
- ];
699
- myClass.onHooks(hooks);
668
+ myClass.onHook({
669
+ id: 'my-hook',
670
+ event: 'before:save',
671
+ handler: async (data) => { data.validated = true; },
672
+ });
700
673
 
701
- // remove all hooks
702
- myClass.removeHook(hooks);
674
+ const hook = myClass.getHook('my-hook');
675
+ console.log(hook?.id); // 'my-hook'
676
+ console.log(hook?.event); // 'before:save'
677
+ console.log(hook?.handler); // [Function]
703
678
  ```
704
679
 
705
- ## .hook(eventName, ...args)
680
+ ## .getHooks(eventName)
706
681
 
707
- Run a hook event.
682
+ Get all hooks for an event. Returns an `IHook[]` array, or `undefined` if no hooks are registered for the event.
708
683
 
709
684
  ```javascript
710
- import { Hookified } from 'hookified';
685
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
686
+ data.some = 'new data';
687
+ }});
711
688
 
712
- class MyClass extends Hookified {
713
- constructor() {
714
- super();
715
- }
689
+ console.log(myClass.getHooks('before:myMethod2')); // [{ event: 'before:myMethod2', handler: [Function] }]
690
+ ```
716
691
 
717
- async myMethodWithHooks() Promise<any> {
718
- let data = { some: 'data' };
719
- // do something
720
- await this.hook('before:myMethod2', data);
692
+ ## .hook(eventName, ...args)
721
693
 
722
- return data;
723
- }
724
- }
694
+ Run a hook event.
695
+
696
+ ```javascript
697
+ // Inside your class method
698
+ await this.hook('before:myMethod2', data);
725
699
  ```
726
700
 
727
- in this example we are passing multiple arguments to the hook:
701
+ You can pass multiple arguments to the hook:
728
702
 
729
703
  ```javascript
730
- import { Hookified } from 'hookified';
704
+ // Inside your class method
705
+ await this.hook('before:myMethod2', data, data2);
731
706
 
732
- class MyClass extends Hookified {
733
- constructor() {
734
- super();
735
- }
707
+ // The handler receives all arguments
708
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data, data2) => {
709
+ data.some = 'new data';
710
+ data2.some = 'new data2';
711
+ }});
712
+ ```
736
713
 
737
- async myMethodWithHooks() Promise<any> {
738
- let data = { some: 'data' };
739
- let data2 = { some: 'data2' };
740
- // do something
741
- await this.hook('before:myMethod2', data, data2);
714
+ ## .hookSync(eventName, ...args)
742
715
 
743
- return data;
744
- }
745
- }
716
+ Run a hook event synchronously. Async handlers (functions declared with `async` keyword) are silently skipped and only synchronous handlers are executed.
746
717
 
747
- const myClass = new MyClass();
718
+ > **Note:** The `.hook()` method is preferred as it executes both sync and async functions. Use `.hookSync()` only when you specifically need synchronous execution.
748
719
 
749
- myClass.onHook('before:myMethod2', async (data, data2) => {
750
- data.some = 'new data';
751
- data2.some = 'new data2';
752
- });
720
+ ```javascript
721
+ // This sync handler will execute
722
+ myClass.onHook({ event: 'before:myMethod', handler: (data) => {
723
+ data.some = 'modified';
724
+ }});
753
725
 
754
- await myClass.myMethodWithHooks();
726
+ // This async handler will be silently skipped
727
+ myClass.onHook({ event: 'before:myMethod', handler: async (data) => {
728
+ data.some = 'will not run';
729
+ }});
730
+
731
+ // Inside your class method
732
+ this.hookSync('before:myMethod', data); // Only sync handler runs
755
733
  ```
756
734
 
757
- ## .callHook(eventName, ...args)
735
+ ## .onHook(hook, options?)
758
736
 
759
- This is an alias for `.hook(eventName, ...args)` for backwards compatibility.
737
+ Subscribe to a hook event. Takes an `IHook` object and an optional `OnHookOptions` object. Returns the stored `IHook` (with `id` assigned), or `undefined` if the hook was blocked by deprecation. The returned reference is the exact object stored internally, which is useful for later removal with `.removeHook()` or `.removeHookById()`. To register multiple hooks at once, use `.onHooks()`.
760
738
 
761
- ## .beforeHook(eventName, ...args)
739
+ If the hook has an `id`, it will be used as-is. If not, a UUID is auto-generated via `crypto.randomUUID()`. If a hook with the same `id` already exists on the same event, it will be **replaced in-place** (preserving its position in the array).
762
740
 
763
- This is a helper function that will prepend a hook name with `before:`.
741
+ **Options (`OnHookOptions`)**:
742
+ - `useHookClone` (boolean, optional) — Per-call override for the instance-level `useHookClone` setting. When `true`, the hook object is cloned before storing. When `false`, the original reference is stored directly. When omitted, falls back to the instance-level setting.
743
+ - `position` (`"Top"` | `"Bottom"` | `number`, optional) — Controls where the hook is inserted in the handlers array. `"Top"` inserts at the beginning, `"Bottom"` appends to the end (default). A number inserts at that index, clamped to the array bounds.
764
744
 
765
745
  ```javascript
766
- import { Hookified } from 'hookified';
767
-
768
- class MyClass extends Hookified {
769
- constructor() {
770
- super();
771
- }
772
-
773
- async myMethodWithHooks() Promise<any> {
774
- let data = { some: 'data' };
775
- // the event name will be `before:myMethod2`
776
- await this.beforeHook('myMethod2', data);
746
+ // Single hook returns the stored IHook with id
747
+ const stored = myClass.onHook({
748
+ event: 'before:myMethod2',
749
+ handler: async (data) => {
750
+ data.some = 'new data';
751
+ },
752
+ });
753
+ console.log(stored.id); // auto-generated UUID
777
754
 
778
- return data;
779
- }
780
- }
781
- ```
755
+ // With a custom id
756
+ const stored2 = myClass.onHook({
757
+ id: 'my-validation',
758
+ event: 'before:save',
759
+ handler: async (data) => { data.validated = true; },
760
+ });
782
761
 
783
- ## .afterHook(eventName, ...args)
762
+ // Replace hook by registering with the same id
763
+ myClass.onHook({
764
+ id: 'my-validation',
765
+ event: 'before:save',
766
+ handler: async (data) => { data.validated = true; data.extra = true; },
767
+ });
768
+ // Only one hook with id 'my-validation' exists, at the same position
784
769
 
785
- This is a helper function that will prepend a hook name with `after:`.
770
+ // Remove by id
771
+ myClass.removeHookById('my-validation');
786
772
 
787
- ```javascript
788
- import { Hookified } from 'hookified';
773
+ // Use the returned reference to remove the hook later
774
+ myClass.removeHook(stored);
789
775
 
790
- class MyClass extends Hookified {
791
- constructor() {
792
- super();
793
- }
776
+ // Override useHookClone per-call — store original reference even though instance default is true
777
+ const hook = { event: 'before:save', handler: async (data) => {} };
778
+ myClass.onHook(hook, { useHookClone: false });
779
+ console.log(myClass.getHooks('before:save')[0] === hook); // true
794
780
 
795
- async myMethodWithHooks() Promise<any> {
796
- let data = { some: 'data' };
797
- // the event name will be `after:myMethod2`
798
- await this.afterHook('myMethod2', data);
781
+ // Insert at the top of the handlers array
782
+ myClass.onHook({ event: 'before:save', handler: async (data) => {} }, { position: 'Top' });
799
783
 
800
- return data;
801
- }
802
- }
784
+ // Insert at a specific index
785
+ myClass.onHook({ event: 'before:save', handler: async (data) => {} }, { position: 1 });
803
786
  ```
804
787
 
805
- ## .hookSync(eventName, ...args)
788
+ ## .onHooks(Array, options?)
806
789
 
807
- Run a hook event synchronously. Async handlers (functions declared with `async` keyword) are silently skipped and only synchronous handlers are executed.
808
-
809
- > **Note:** The `.hook()` method is preferred as it executes both sync and async functions. Use `.hookSync()` only when you specifically need synchronous execution.
790
+ Subscribe to multiple hook events at once. Takes an array of `IHook` objects and an optional `OnHookOptions` object that is applied to each hook.
810
791
 
811
792
  ```javascript
812
- import { Hookified } from 'hookified';
813
-
814
- class MyClass extends Hookified {
815
- constructor() {
816
- super();
817
- }
793
+ const hooks = [
794
+ {
795
+ event: 'before:myMethodWithHooks',
796
+ handler: async (data) => {
797
+ data.some = 'new data1';
798
+ },
799
+ },
800
+ {
801
+ event: 'after:myMethodWithHooks',
802
+ handler: async (data) => {
803
+ data.some = 'new data2';
804
+ },
805
+ },
806
+ ];
807
+ myClass.onHooks(hooks);
818
808
 
819
- myMethodWithSyncHooks() {
820
- let data = { some: 'data' };
821
- // Only synchronous handlers will execute
822
- this.hookSync('before:myMethod', data);
809
+ // With options — insert all hooks at the top
810
+ myClass.onHooks(hooks, { position: 'Top' });
823
811
 
824
- return data;
825
- }
826
- }
812
+ // With options — skip cloning for all hooks in this batch
813
+ myClass.onHooks(hooks, { useHookClone: false });
814
+ ```
827
815
 
828
- const myClass = new MyClass();
816
+ ## .onceHook(hook)
829
817
 
830
- // This sync handler will execute
831
- myClass.onHook('before:myMethod', (data) => {
832
- data.some = 'modified';
833
- });
818
+ Subscribe to a hook event once. Takes an `IHook` object with `event` and `handler` properties. After the handler is called once, it is automatically removed.
834
819
 
835
- // This async handler will be silently skipped
836
- myClass.onHook('before:myMethod', async (data) => {
837
- data.some = 'will not run';
838
- });
820
+ ```javascript
821
+ myClass.onceHook({ event: 'before:myMethod2', handler: async (data) => {
822
+ data.some = 'new data';
823
+ }});
839
824
 
840
- myClass.myMethodWithSyncHooks(); // Only sync handler runs
825
+ await myClass.hook('before:myMethod2', data); // handler runs once then is removed
826
+ console.log(myClass.hooks.size); // 0
841
827
  ```
842
828
 
843
- ## .hooks
829
+ ## .prependHook(hook, options?)
844
830
 
845
- Get all hooks.
831
+ Subscribe to a hook event before all other hooks. Takes an `IHook` object with `event` and `handler` properties. Returns the stored `IHook` (with generated `id`), or `undefined` if blocked by deprecation. Equivalent to calling `onHook(hook, { position: "Top" })`.
832
+
833
+ An optional `PrependHookOptions` object can be passed with:
834
+ - `useHookClone` (boolean) — per-call override for hook cloning behavior
846
835
 
847
836
  ```javascript
848
- import { Hookified } from 'hookified';
837
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
838
+ data.some = 'new data';
839
+ }});
840
+ myClass.prependHook({ event: 'before:myMethod2', handler: async (data) => {
841
+ data.some = 'will run before new data';
842
+ }});
843
+ ```
849
844
 
850
- class MyClass extends Hookified {
851
- constructor() {
852
- super();
853
- }
845
+ ## .prependOnceHook(hook, options?)
854
846
 
855
- async myMethodWithHooks() Promise<any> {
856
- let data = { some: 'data' };
857
- // do something
858
- await this.hook('before:myMethod2', data);
847
+ Subscribe to a hook event before all other hooks. Takes an `IHook` object with `event` and `handler` properties. After the handler is called once, it is automatically removed. Returns the stored `IHook` (with generated `id`), or `undefined` if blocked by deprecation.
859
848
 
860
- return data;
861
- }
862
- }
849
+ An optional `PrependHookOptions` object can be passed with:
850
+ - `useHookClone` (boolean) — per-call override for hook cloning behavior
863
851
 
864
- const myClass = new MyClass();
865
- myClass.onHook('before:myMethod2', async (data) => {
852
+ ```javascript
853
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
866
854
  data.some = 'new data';
867
- });
868
-
869
- console.log(myClass.hooks);
855
+ }});
856
+ myClass.prependOnceHook({ event: 'before:myMethod2', handler: async (data) => {
857
+ data.some = 'will run before new data';
858
+ }});
870
859
  ```
871
860
 
872
- ## .getHooks(eventName)
861
+ ## .removeEventHooks(eventName)
873
862
 
874
- Get all hooks for an event.
863
+ Removes all hooks for a specific event and returns the removed hooks as an `IHook[]` array. Returns an empty array if no hooks are registered for the event.
875
864
 
876
865
  ```javascript
877
- import { Hookified } from 'hookified';
878
-
879
- class MyClass extends Hookified {
880
- constructor() {
881
- super();
882
- }
866
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
867
+ data.some = 'new data';
868
+ }});
869
+ myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
870
+ data.some = 'more data';
871
+ }});
872
+
873
+ // Remove all hooks for a specific event
874
+ const removed = myClass.removeEventHooks('before:myMethod2');
875
+ console.log(removed.length); // 2
876
+ ```
883
877
 
884
- async myMethodWithHooks() Promise<any> {
885
- let data = { some: 'data' };
886
- // do something
887
- await this.hook('before:myMethod2', data);
878
+ ## .removeHook(hook)
888
879
 
889
- return data;
890
- }
891
- }
880
+ Unsubscribe a handler from a hook event. Takes an `IHook` object with `event` and `handler` properties. Returns the removed hook as an `IHook` object, or `undefined` if the handler was not found.
892
881
 
893
- const myClass = new MyClass();
894
- myClass.onHook('before:myMethod2', async (data) => {
882
+ ```javascript
883
+ const handler = async (data) => {
895
884
  data.some = 'new data';
896
- });
885
+ };
886
+
887
+ myClass.onHook({ event: 'before:myMethod2', handler });
897
888
 
898
- console.log(myClass.getHooks('before:myMethod2'));
889
+ const removed = myClass.removeHook({ event: 'before:myMethod2', handler });
890
+ console.log(removed); // { event: 'before:myMethod2', handler: [Function] }
899
891
  ```
900
892
 
901
- ## .clearHooks(eventName)
893
+ ## .removeHookById(id)
894
+
895
+ Remove one or more hooks by `id`, searching across all events. Accepts a single `string` or an array of `string` ids.
902
896
 
903
- Clear all hooks for an event.
897
+ - **Single id**: Returns the removed `IHook`, or `undefined` if not found.
898
+ - **Array of ids**: Returns an `IHook[]` array of the hooks that were successfully removed.
899
+
900
+ When the last hook for an event is removed, the event key is cleaned up.
904
901
 
905
902
  ```javascript
906
- import { Hookified } from 'hookified';
903
+ const myClass = new MyClass();
907
904
 
908
- class MyClass extends Hookified {
909
- constructor() {
910
- super();
911
- }
905
+ myClass.onHook({ id: 'hook-a', event: 'before:save', handler: async () => {} });
906
+ myClass.onHook({ id: 'hook-b', event: 'after:save', handler: async () => {} });
907
+ myClass.onHook({ id: 'hook-c', event: 'before:save', handler: async () => {} });
912
908
 
913
- async myMethodWithHooks() Promise<any> {
914
- let data = { some: 'data' };
915
- // do something
916
- await this.hook('before:myMethod2', data);
909
+ // Remove a single hook by id
910
+ const removed = myClass.removeHookById('hook-a');
911
+ console.log(removed?.id); // 'hook-a'
917
912
 
918
- return data;
919
- }
920
- }
913
+ // Remove multiple hooks by ids
914
+ const removedMany = myClass.removeHookById(['hook-b', 'hook-c']);
915
+ console.log(removedMany.length); // 2
916
+ ```
921
917
 
922
- const myClass = new MyClass();
918
+ ## .removeHooks(Array)
923
919
 
924
- myClass.onHook('before:myMethod2', async (data) => {
925
- data.some = 'new data';
926
- });
920
+ Unsubscribe from multiple hooks. Returns an array of the hooks that were successfully removed.
921
+
922
+ ```javascript
923
+ const hooks = [
924
+ { event: 'before:save', handler: async (data) => { data.some = 'new data1'; } },
925
+ { event: 'after:save', handler: async (data) => { data.some = 'new data2'; } },
926
+ ];
927
+ myClass.onHooks(hooks);
927
928
 
928
- myClass.clearHooks('before:myMethod2');
929
+ const removed = myClass.removeHooks(hooks);
930
+ console.log(removed.length); // 2
929
931
  ```
930
932
 
931
933
  # API - Events
932
934
 
935
+ > All examples below assume the following setup unless otherwise noted:
936
+ > ```javascript
937
+ > import { Hookified } from 'hookified';
938
+ > class MyClass extends Hookified {
939
+ > constructor(options) { super(options); }
940
+ > }
941
+ > const myClass = new MyClass();
942
+ > ```
943
+
933
944
  ## .throwOnEmitError
934
945
 
935
- If set to true, errors emitted as `error` will be thrown if there are no listeners. If set to false, errors will be only emitted.
946
+ If set to true, errors emitted as `error` will always be thrown, even if there are listeners. If set to false (default), errors will only be emitted to listeners.
936
947
 
937
948
  ```javascript
938
- import { Hookified } from 'hookified';
939
-
940
- class MyClass extends Hookified {
941
- constructor() {
942
- super();
943
- }
949
+ const myClass = new MyClass({ throwOnEmitError: true });
944
950
 
945
- async myMethodWithHooks() Promise<any> {
946
- let data = { some: 'data' };
947
- // do something
948
- await this.hook('before:myMethod2', data);
951
+ myClass.on('error', (err) => {
952
+ console.log('listener received:', err.message);
953
+ });
949
954
 
950
- return data;
951
- }
955
+ try {
956
+ myClass.emit('error', new Error('This will throw despite having a listener'));
957
+ } catch (error) {
958
+ console.log(error.message); // This will throw despite having a listener
952
959
  }
953
960
  ```
954
961
 
955
962
  ## .throwOnEmptyListeners
956
963
 
957
- If set to true, errors will be thrown when emitting an `error` event with no listeners. This follows the standard Node.js EventEmitter behavior. Default is false. In version 2, this will be set to true by default.
964
+ If set to true, errors will be thrown when emitting an `error` event with no listeners. This follows the standard Node.js EventEmitter behavior. Default is `true`.
958
965
 
959
966
  ```javascript
960
- import { Hookified } from 'hookified';
961
-
962
- class MyClass extends Hookified {
963
- constructor() {
964
- super({ throwOnEmptyListeners: true });
965
- }
966
- }
967
-
968
- const myClass = new MyClass();
967
+ const myClass = new MyClass({ throwOnEmptyListeners: true });
969
968
 
970
- console.log(myClass.throwOnEmptyListeners); // true
969
+ console.log(myClass.throwOnEmptyListeners); // true (default)
971
970
 
972
971
  // This will throw because there are no error listeners
973
972
  try {
@@ -999,20 +998,6 @@ When both are set to `true`, `throwOnEmitError` takes precedence.
999
998
  Subscribe to an event.
1000
999
 
1001
1000
  ```javascript
1002
- import { Hookified } from 'hookified';
1003
-
1004
- class MyClass extends Hookified {
1005
- constructor() {
1006
- super();
1007
- }
1008
-
1009
- async myMethodEmittingEvent() {
1010
- this.emit('message', 'Hello World');
1011
- }
1012
- }
1013
-
1014
- const myClass = new MyClass();
1015
-
1016
1001
  myClass.on('message', (message) => {
1017
1002
  console.log(message);
1018
1003
  });
@@ -1023,26 +1008,12 @@ myClass.on('message', (message) => {
1023
1008
  Unsubscribe from an event.
1024
1009
 
1025
1010
  ```javascript
1026
- import { Hookified } from 'hookified';
1027
-
1028
- class MyClass extends Hookified {
1029
- constructor() {
1030
- super();
1031
- }
1032
-
1033
- async myMethodEmittingEvent() {
1034
- this.emit('message', 'Hello World');
1035
- }
1036
- }
1037
-
1038
- const myClass = new MyClass();
1039
- myClass.on('message', (message) => {
1011
+ const handler = (message) => {
1040
1012
  console.log(message);
1041
- });
1013
+ };
1042
1014
 
1043
- myClass.off('message', (message) => {
1044
- console.log(message);
1045
- });
1015
+ myClass.on('message', handler);
1016
+ myClass.off('message', handler);
1046
1017
  ```
1047
1018
 
1048
1019
  ## .emit(eventName, ...args)
@@ -1050,17 +1021,7 @@ myClass.off('message', (message) => {
1050
1021
  Emit an event.
1051
1022
 
1052
1023
  ```javascript
1053
- import { Hookified } from 'hookified';
1054
-
1055
- class MyClass extends Hookified {
1056
- constructor() {
1057
- super();
1058
- }
1059
-
1060
- async myMethodEmittingEvent() {
1061
- this.emit('message', 'Hello World');
1062
- }
1063
- }
1024
+ myClass.emit('message', 'Hello World');
1064
1025
  ```
1065
1026
 
1066
1027
  ## .listeners(eventName)
@@ -1068,20 +1029,6 @@ class MyClass extends Hookified {
1068
1029
  Get all listeners for an event.
1069
1030
 
1070
1031
  ```javascript
1071
- import { Hookified } from 'hookified';
1072
-
1073
- class MyClass extends Hookified {
1074
- constructor() {
1075
- super();
1076
- }
1077
-
1078
- async myMethodEmittingEvent() {
1079
- this.emit('message', 'Hello World');
1080
- }
1081
- }
1082
-
1083
- const myClass = new MyClass();
1084
-
1085
1032
  myClass.on('message', (message) => {
1086
1033
  console.log(message);
1087
1034
  });
@@ -1094,20 +1041,6 @@ console.log(myClass.listeners('message'));
1094
1041
  Remove all listeners for an event.
1095
1042
 
1096
1043
  ```javascript
1097
- import { Hookified } from 'hookified';
1098
-
1099
- class MyClass extends Hookified {
1100
- constructor() {
1101
- super();
1102
- }
1103
-
1104
- async myMethodEmittingEvent() {
1105
- this.emit('message', 'Hello World');
1106
- }
1107
- }
1108
-
1109
- const myClass = new MyClass();
1110
-
1111
1044
  myClass.on('message', (message) => {
1112
1045
  console.log(message);
1113
1046
  });
@@ -1117,23 +1050,9 @@ myClass.removeAllListeners('message');
1117
1050
 
1118
1051
  ## .setMaxListeners(maxListeners: number)
1119
1052
 
1120
- Set the maximum number of listeners and will truncate if there are already too many.
1121
-
1122
- ```javascript
1123
- import { Hookified } from 'hookified';
1124
-
1125
- class MyClass extends Hookified {
1126
- constructor() {
1127
- super();
1128
- }
1129
-
1130
- async myMethodEmittingEvent() {
1131
- this.emit('message', 'Hello World');
1132
- }
1133
- }
1134
-
1135
- const myClass = new MyClass();
1053
+ Set the maximum number of listeners for a single event. Default is `0` (unlimited). Negative values are treated as `0`. Setting to `0` disables the limit and the warning. When the limit is exceeded, a `MaxListenersExceededWarning` is emitted via `console.warn` but the listener is still added. This matches standard Node.js EventEmitter behavior.
1136
1054
 
1055
+ ```javascript
1137
1056
  myClass.setMaxListeners(1);
1138
1057
 
1139
1058
  myClass.on('message', (message) => {
@@ -1142,9 +1061,9 @@ myClass.on('message', (message) => {
1142
1061
 
1143
1062
  myClass.on('message', (message) => {
1144
1063
  console.log(message);
1145
- }); // this will not be added and console warning
1064
+ }); // warning emitted but listener is still added
1146
1065
 
1147
- console.log(myClass.listenerCount('message')); // 1
1066
+ console.log(myClass.listenerCount('message')); // 2
1148
1067
  ```
1149
1068
 
1150
1069
  ## .once(eventName, handler)
@@ -1152,23 +1071,12 @@ console.log(myClass.listenerCount('message')); // 1
1152
1071
  Subscribe to an event once.
1153
1072
 
1154
1073
  ```javascript
1155
- import { Hookified } from 'hookified';
1156
-
1157
- class MyClass extends Hookified {
1158
- constructor() {
1159
- super();
1160
- }
1161
- }
1162
-
1163
- const myClass = new MyClass();
1164
-
1165
1074
  myClass.once('message', (message) => {
1166
1075
  console.log(message);
1167
1076
  });
1168
1077
 
1169
- myClass.emit('message', 'Hello World');
1170
-
1171
- myClass.emit('message', 'Hello World'); // this will not be called
1078
+ myClass.emit('message', 'Hello World'); // handler runs
1079
+ myClass.emit('message', 'Hello World'); // handler does not run
1172
1080
  ```
1173
1081
 
1174
1082
  ## .prependListener(eventName, handler)
@@ -1176,16 +1084,6 @@ myClass.emit('message', 'Hello World'); // this will not be called
1176
1084
  Prepend a listener to an event. This will be called before any other listeners.
1177
1085
 
1178
1086
  ```javascript
1179
- import { Hookified } from 'hookified';
1180
-
1181
- class MyClass extends Hookified {
1182
- constructor() {
1183
- super();
1184
- }
1185
- }
1186
-
1187
- const myClass = new MyClass();
1188
-
1189
1087
  myClass.prependListener('message', (message) => {
1190
1088
  console.log(message);
1191
1089
  });
@@ -1196,16 +1094,6 @@ myClass.prependListener('message', (message) => {
1196
1094
  Prepend a listener to an event once. This will be called before any other listeners.
1197
1095
 
1198
1096
  ```javascript
1199
- import { Hookified } from 'hookified';
1200
-
1201
- class MyClass extends Hookified {
1202
- constructor() {
1203
- super();
1204
- }
1205
- }
1206
-
1207
- const myClass = new MyClass();
1208
-
1209
1097
  myClass.prependOnceListener('message', (message) => {
1210
1098
  console.log(message);
1211
1099
  });
@@ -1218,38 +1106,18 @@ myClass.emit('message', 'Hello World');
1218
1106
  Get all event names.
1219
1107
 
1220
1108
  ```javascript
1221
- import { Hookified } from 'hookified';
1222
-
1223
- class MyClass extends Hookified {
1224
- constructor() {
1225
- super();
1226
- }
1227
- }
1228
-
1229
- const myClass = new MyClass();
1230
-
1231
1109
  myClass.on('message', (message) => {
1232
1110
  console.log(message);
1233
1111
  });
1234
1112
 
1235
- console.log(myClass.eventNames());
1113
+ console.log(myClass.eventNames()); // ['message']
1236
1114
  ```
1237
1115
 
1238
1116
  ## .listenerCount(eventName?)
1239
1117
 
1240
- Get the count of listeners for an event or all events if evenName not provided.
1118
+ Get the count of listeners for an event or all events if eventName not provided.
1241
1119
 
1242
1120
  ```javascript
1243
- import { Hookified } from 'hookified';
1244
-
1245
- class MyClass extends Hookified {
1246
- constructor() {
1247
- super();
1248
- }
1249
- }
1250
-
1251
- const myClass = new MyClass();
1252
-
1253
1121
  myClass.on('message', (message) => {
1254
1122
  console.log(message);
1255
1123
  });
@@ -1259,19 +1127,9 @@ console.log(myClass.listenerCount('message')); // 1
1259
1127
 
1260
1128
  ## .rawListeners(eventName?)
1261
1129
 
1262
- Get all listeners for an event or all events if evenName not provided.
1130
+ Get all listeners for an event or all events if eventName not provided.
1263
1131
 
1264
1132
  ```javascript
1265
- import { Hookified } from 'hookified';
1266
-
1267
- class MyClass extends Hookified {
1268
- constructor() {
1269
- super();
1270
- }
1271
- }
1272
-
1273
- const myClass = new MyClass();
1274
-
1275
1133
  myClass.on('message', (message) => {
1276
1134
  console.log(message);
1277
1135
  });
@@ -1281,20 +1139,20 @@ console.log(myClass.rawListeners('message'));
1281
1139
 
1282
1140
  # Logging
1283
1141
 
1284
- Hookified integrates logging directly into the event system. When a logger is configured, all emitted events are automatically logged to the appropriate log level based on the event name.
1142
+ Hookified integrates logging directly into the event system. When an `eventLogger` is configured, all emitted events are automatically logged to the appropriate log level based on the event name.
1285
1143
 
1286
1144
  ## How It Works
1287
1145
 
1288
- When you emit an event, Hookified automatically sends the event data to the configured logger using the appropriate log method:
1146
+ When you emit an event, Hookified automatically sends the event data to the configured `eventLogger` using the appropriate log method:
1289
1147
 
1290
1148
  | Event Name | Logger Method |
1291
1149
  |------------|---------------|
1292
- | `error` | `logger.error()` |
1293
- | `warn` | `logger.warn()` |
1294
- | `debug` | `logger.debug()` |
1295
- | `trace` | `logger.trace()` |
1296
- | `fatal` | `logger.fatal()` |
1297
- | Any other | `logger.info()` |
1150
+ | `error` | `eventLogger.error()` |
1151
+ | `warn` | `eventLogger.warn()` |
1152
+ | `debug` | `eventLogger.debug()` |
1153
+ | `trace` | `eventLogger.trace()` |
1154
+ | `fatal` | `eventLogger.fatal()` |
1155
+ | Any other | `eventLogger.info()` |
1298
1156
 
1299
1157
  The logger receives two arguments:
1300
1158
  1. **message**: A string extracted from the event data (error messages, object messages, or JSON stringified data)
@@ -1325,7 +1183,7 @@ const logger = pino();
1325
1183
 
1326
1184
  class MyService extends Hookified {
1327
1185
  constructor() {
1328
- super({ logger });
1186
+ super({ eventLogger: logger });
1329
1187
  }
1330
1188
 
1331
1189
  async processData(data) {
@@ -1351,14 +1209,14 @@ service.emit('error', new Error('Failed')); // -> logger.error()
1351
1209
  service.emit('custom-event', { foo: 'bar' }); // -> logger.info() (default)
1352
1210
  ```
1353
1211
 
1354
- You can also set or change the logger after instantiation:
1212
+ You can also set or change the eventLogger after instantiation:
1355
1213
 
1356
1214
  ```javascript
1357
1215
  const service = new MyService();
1358
- service.logger = pino({ level: 'debug' });
1216
+ service.eventLogger = pino({ level: 'debug' });
1359
1217
 
1360
- // Or remove the logger
1361
- service.logger = undefined;
1218
+ // Or remove the eventLogger
1219
+ service.eventLogger = undefined;
1362
1220
  ```
1363
1221
 
1364
1222
  # Benchmarks
@@ -1367,10 +1225,10 @@ We are doing very simple benchmarking to see how this compares to other librarie
1367
1225
 
1368
1226
  ## Hooks
1369
1227
 
1370
- | name | summary | ops/sec | time/op | margin | samples |
1371
- |-----------------------|:---------:|----------:|----------:|:--------:|----------:|
1372
- | Hookified (v1.14.0) | 🥇 | 3M | 318ns | ±0.01% | 3M |
1373
- | Hookable (v6.0.1) | -57% | 1M | 834ns | ±0.01% | 1M |
1228
+ | name | summary | ops/sec | time/op | margin | samples |
1229
+ |----------------------|:---------:|----------:|----------:|:--------:|----------:|
1230
+ | Hookified (v2.0.0) | 🥇 | 5M | 214ns | ±0.01% | 5M |
1231
+ | Hookable (v6.0.1) | -59% | 2M | 567ns | ±0.01% | 2M |
1374
1232
 
1375
1233
  ## Emits
1376
1234
 
@@ -1378,30 +1236,427 @@ This shows how on par `hookified` is to the native `EventEmitter` and popular `e
1378
1236
 
1379
1237
  | name | summary | ops/sec | time/op | margin | samples |
1380
1238
  |---------------------------|:---------:|----------:|----------:|:--------:|----------:|
1381
- | Hookified (v1.13.0) | 🥇 | 12M | 90ns | ±3.17% | 11M |
1382
- | EventEmitter3 (v5.0.1) | -0.52% | 12M | 89ns | ±1.66% | 11M |
1383
- | EventEmitter (v20.17.0) | -3.5% | 12M | 91ns | ±0.42% | 11M |
1384
- | Emittery (v1.2.0) | -91% | 1M | 1µs | ±3.33% | 959K |
1239
+ | EventEmitter3 (v5.0.4) | 🥇 | 14M | 82ns | ±0.02% | 12M |
1240
+ | Hookified (v2.0.0) | -6.9% | 13M | 97ns | ±0.02% | 10M |
1241
+ | EventEmitter (v24.11.1) | -7.2% | 13M | 83ns | ±0.02% | 12M |
1242
+ | Emittery (v1.2.0) | -92% | 1M | 1µs | ±0.01% | 979K ||
1385
1243
 
1386
1244
  _Note: the `EventEmitter` version is Nodejs versioning._
1387
1245
 
1388
- # How to Contribute
1246
+ # Migrating from v1 to v2
1389
1247
 
1390
- Hookified is written in TypeScript and tests are written in `vitest`. To run the tests, use the following command:
1248
+ ## Quick Guide
1391
1249
 
1392
- To setup the environment and run the tests:
1250
+ v2 overhauls hook storage to use `IHook` objects instead of raw functions. This enables hook IDs, ordering via position, cloning control, and new hook types like `WaterfallHook`. The main change most users will notice is that `onHook` now takes an `IHook` object instead of positional arguments:
1393
1251
 
1394
- ```bash
1395
- pnpm i && pnpm test
1252
+ ```typescript
1253
+ // v1 positional arguments
1254
+ hookified.onHook('before:save', async (data) => {});
1255
+
1256
+ // v2 — IHook object (or use addHook for positional args)
1257
+ hookified.onHook({ event: 'before:save', handler: async (data) => {} });
1258
+ hookified.addHook('before:save', async (data) => {}); // still works
1396
1259
  ```
1397
1260
 
1398
- Note that we are using `pnpm` as our package manager. If you don't have it installed, you can install it globally with:
1261
+ **Other common changes:**
1262
+
1263
+ | v1 | v2 |
1264
+ |---|---|
1265
+ | `throwHookErrors` | `throwOnHookError` |
1266
+ | `logger` | `eventLogger` |
1267
+ | `onHookEntry(hook)` | `onHook(hook)` |
1268
+ | `HookEntry` type | `IHook` interface |
1269
+ | `Hook` type (fn) | `HookFn` type |
1270
+ | `getHooks()` returns `HookFn[]` | `getHooks()` returns `IHook[]` |
1271
+ | `removeHook(event, handler)` | `removeHook({ event, handler })` |
1272
+
1273
+ See below for full details on each change.
1274
+
1275
+ **[Breaking Changes](#breaking-changes)**
1276
+ - [`throwHookErrors` removed — use `throwOnHookError` instead](#throwhookerrors-removed--use-throwonhookerror-instead)
1277
+ - [`throwOnEmptyListeners` now defaults to `true`](#throwonemptylisteners-now-defaults-to-true)
1278
+ - [`logger` renamed to `eventLogger`](#logger-renamed-to-eventlogger)
1279
+ - [`maxListeners` default changed from `100` to `0` (unlimited) and no longer truncates](#maxlisteners-default-changed-from-100-to-0-unlimited-and-no-longer-truncates)
1280
+ - [`onHookEntry` removed — use `onHook` instead](#onhookentry-removed--use-onhook-instead)
1281
+ - [`onHook` signature changed](#onhook-signature-changed)
1282
+ - [`HookEntry` type and `Hook` type removed](#hookentry-type-and-hook-type-removed)
1283
+ - [`removeHook` and `removeHooks` now return removed hooks](#removehook-and-removehooks-now-return-removed-hooks)
1284
+ - [`removeHook`, `removeHooks`, and `getHooks` no longer check for deprecated hooks](#removehook-removehooks-and-gethooks-no-longer-check-for-deprecated-hooks)
1285
+ - [Internal hook storage now uses `IHook` objects](#internal-hook-storage-now-uses-ihook-objects)
1286
+ - [`onceHook`, `prependHook`, `prependOnceHook`, and `removeHook` now take `IHook`](#oncehook-prependhook-prependoncehook-and-removehook-now-take-ihook)
1287
+ - [`onHook` now returns the stored hook](#onhook-now-returns-the-stored-hook)
1288
+
1289
+ **[New Features](#new-features)**
1290
+ - [standard `Hook` class now available](#standard-hook)
1291
+ - [`WaterfallHook` class for sequential data transformation pipelines](#waterfallhook-class)
1292
+ - [`useHookClone` option](#usehookclone-option)
1293
+ - [`onHook` now accepts `OnHookOptions`](#onhook-now-accepts-onhookoptions)
1294
+ - [`IHook` now has an `id` property](#ihook-now-has-an-id-property)
1295
+ - [`removeEventHooks` method](#removeeventhooks-method)
1296
+
1297
+ ## Breaking Changes
1298
+
1299
+ | Change | Summary |
1300
+ |---|---|
1301
+ | `throwHookErrors` | Renamed to `throwOnHookError` |
1302
+ | `throwOnEmptyListeners` | Default changed from `false` to `true` |
1303
+ | `logger` | Renamed to `eventLogger` |
1304
+ | `maxListeners` | Default changed from `100` to `0` (unlimited), no longer truncates |
1305
+ | `onHookEntry` | Removed — use `onHook` instead |
1306
+ | `onHook` signature | Now takes `IHook` object instead of `(event, handler)` |
1307
+ | `HookEntry` / `Hook` types | Replaced with `IHook` / `HookFn` |
1308
+ | `removeHook` / `removeHooks` | Now return removed hooks; no longer check deprecated status |
1309
+ | Internal hook storage | Uses `IHook` objects instead of raw functions |
1310
+ | `onceHook`, `prependHook`, etc. | Now take `IHook` instead of `(event, handler)` |
1311
+ | `onHook` return | Now returns stored `IHook` (was `void`) |
1312
+
1313
+ ### `throwHookErrors` removed — use `throwOnHookError` instead
1314
+
1315
+ The deprecated `throwHookErrors` option and property has been removed. Use `throwOnHookError` instead.
1316
+
1317
+ **Before (v1):**
1399
1318
 
1400
- ```bash
1401
- npm install -g pnpm
1319
+ ```javascript
1320
+ super({ throwHookErrors: true });
1321
+ myClass.throwHookErrors = false;
1402
1322
  ```
1403
1323
 
1404
- To contribute follow the [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
1324
+ **After (v2):**
1325
+
1326
+ ```javascript
1327
+ super({ throwOnHookError: true });
1328
+ myClass.throwOnHookError = false;
1329
+ ```
1330
+
1331
+ ### `throwOnEmptyListeners` now defaults to `true`
1332
+
1333
+ The `throwOnEmptyListeners` option now defaults to `true`, matching standard Node.js EventEmitter behavior. Previously it defaulted to `false`. If you emit an `error` event with no listeners registered, an error will now be thrown by default.
1334
+
1335
+ **Before (v1):**
1336
+
1337
+ ```javascript
1338
+ const myClass = new MyClass(); // throwOnEmptyListeners defaults to false
1339
+ myClass.emit('error', new Error('No throw')); // silently ignored
1340
+ ```
1341
+
1342
+ **After (v2):**
1343
+
1344
+ ```javascript
1345
+ const myClass = new MyClass(); // throwOnEmptyListeners defaults to true
1346
+ myClass.emit('error', new Error('This will throw')); // throws!
1347
+
1348
+ // To restore v1 behavior:
1349
+ const myClass2 = new MyClass({ throwOnEmptyListeners: false });
1350
+ ```
1351
+
1352
+ ### `logger` renamed to `eventLogger`
1353
+
1354
+ The `logger` option and property has been renamed to `eventLogger` to avoid conflicts with other logger properties in your classes.
1355
+
1356
+ **Before (v1):**
1357
+
1358
+ ```javascript
1359
+ super({ logger });
1360
+ myClass.logger = pino({ level: 'debug' });
1361
+ ```
1362
+
1363
+ **After (v2):**
1364
+
1365
+ ```javascript
1366
+ super({ eventLogger: logger });
1367
+ myClass.eventLogger = pino({ level: 'debug' });
1368
+ ```
1369
+
1370
+ ### `maxListeners` default changed from `100` to `0` (unlimited) and no longer truncates
1371
+
1372
+ The default maximum number of listeners has changed from `100` to `0` (unlimited). The `MaxListenersExceededWarning` will no longer be emitted unless you explicitly set a limit via `setMaxListeners()`. Additionally, `setMaxListeners()` no longer truncates existing listeners — it only sets the warning threshold, matching standard Node.js EventEmitter behavior.
1373
+
1374
+ **Before (v1):**
1375
+
1376
+ ```javascript
1377
+ const myClass = new MyClass(); // maxListeners defaults to 100
1378
+ // Warning emitted after adding 100+ listeners to the same event
1379
+ // setMaxListeners() would truncate existing listeners exceeding the limit
1380
+ ```
1381
+
1382
+ **After (v2):**
1383
+
1384
+ ```javascript
1385
+ const myClass = new MyClass(); // maxListeners defaults to 0 (unlimited)
1386
+ // No warning — unlimited listeners allowed
1387
+ // setMaxListeners() only sets warning threshold, never removes listeners
1388
+
1389
+ // To restore v1 warning behavior:
1390
+ myClass.setMaxListeners(100);
1391
+ ```
1392
+
1393
+ ### `onHookEntry` removed — use `onHook` instead
1394
+
1395
+ The `onHookEntry` method has been removed. Use `onHook` which now accepts an `IHook` object (or array of `IHook`) directly.
1396
+
1397
+ **Before (v1):**
1398
+
1399
+ ```typescript
1400
+ hookified.onHookEntry({ event: 'before:save', handler: async (data) => {} });
1401
+ ```
1402
+
1403
+ **After (v2):**
1404
+
1405
+ ```typescript
1406
+ hookified.onHook({ event: 'before:save', handler: async (data) => {} });
1407
+ ```
1408
+
1409
+ ### `onHook` signature changed
1410
+
1411
+ `onHook` no longer accepts positional `(event, handler)` arguments. It now takes a single `IHook` object or `Hook` class instance. Use `addHook(event, handler)` if you prefer positional arguments. Use `onHooks()` for bulk registration.
1412
+
1413
+ **Before (v1):**
1414
+
1415
+ ```typescript
1416
+ hookified.onHook('before:save', async (data) => {});
1417
+ ```
1418
+
1419
+ **After (v2):**
1420
+
1421
+ ```typescript
1422
+ // Using IHook object
1423
+ hookified.onHook({ event: 'before:save', handler: async (data) => {} });
1424
+
1425
+ // For multiple hooks, use onHooks
1426
+ hookified.onHooks([
1427
+ { event: 'before:save', handler: async (data) => {} },
1428
+ { event: 'after:save', handler: async (data) => {} },
1429
+ ]);
1430
+
1431
+ // Or use addHook for positional args
1432
+ hookified.addHook('before:save', async (data) => {});
1433
+ ```
1434
+
1435
+ ### `HookEntry` type and `Hook` type removed
1436
+
1437
+ The `HookEntry` type has been removed and replaced with the `IHook` interface. The `Hook` type (function type) has been renamed to `HookFn`.
1438
+
1439
+ **Before (v1):**
1440
+
1441
+ ```typescript
1442
+ import type { HookEntry, Hook } from 'hookified';
1443
+
1444
+ const hook: HookEntry = { event: 'before:save', handler: async () => {} };
1445
+ const myHook: Hook = async (data) => {};
1446
+ ```
1447
+
1448
+ **After (v2):**
1449
+
1450
+ ```typescript
1451
+ import type { IHook, HookFn } from 'hookified';
1452
+
1453
+ const hook: IHook = { event: 'before:save', handler: async () => {} };
1454
+ const myHook: HookFn = async (data) => {};
1455
+ ```
1456
+
1457
+ ### `removeHook` and `removeHooks` now return removed hooks
1458
+
1459
+ `removeHook` now returns the removed hook as an `IHook` object (or `undefined` if not found). `removeHooks` now returns an `IHook[]` array of the hooks that were successfully removed. Previously both returned `void`.
1460
+
1461
+ **Before (v1):**
1462
+
1463
+ ```typescript
1464
+ hookified.removeHook('before:save', handler); // void
1465
+ hookified.removeHooks(hooks); // void
1466
+ ```
1467
+
1468
+ **After (v2):**
1469
+
1470
+ ```typescript
1471
+ const removed = hookified.removeHook({ event: 'before:save', handler }); // IHook | undefined
1472
+ const removedHooks = hookified.removeHooks(hooks); // IHook[]
1473
+ ```
1474
+
1475
+ ### `removeHook`, `removeHooks`, and `getHooks` no longer check for deprecated hooks
1476
+
1477
+ Previously, `removeHook`, `removeHooks`, and `getHooks` would skip their operation and emit a deprecation warning when called with a deprecated hook name and `allowDeprecated` was `false`. This made it impossible to clean up or inspect deprecated hooks. These methods now always operate regardless of deprecation status.
1478
+
1479
+ ### Internal hook storage now uses `IHook` objects
1480
+
1481
+ The internal `_hooks` map now stores full `IHook` objects (`Map<string, IHook[]>`) instead of raw handler functions (`Map<string, HookFn[]>`). This means `.hooks` returns `Map<string, IHook[]>` and `.getHooks()` returns `IHook[] | undefined`.
1482
+
1483
+ **Before (v1):**
1484
+
1485
+ ```typescript
1486
+ const hooks = myClass.getHooks('before:save'); // HookFn[]
1487
+ hooks[0](data); // direct function call
1488
+ ```
1489
+
1490
+ **After (v2):**
1491
+
1492
+ ```typescript
1493
+ const hooks = myClass.getHooks('before:save'); // IHook[]
1494
+ hooks[0].handler(data); // access .handler property
1495
+ hooks[0].event; // 'before:save'
1496
+ ```
1497
+
1498
+ ### `onceHook`, `prependHook`, `prependOnceHook`, and `removeHook` now take `IHook`
1499
+
1500
+ These methods now accept an `IHook` object instead of separate `(event, handler)` arguments.
1501
+
1502
+ **Before (v1):**
1503
+
1504
+ ```typescript
1505
+ hookified.onceHook('before:save', async (data) => {});
1506
+ hookified.prependHook('before:save', async (data) => {});
1507
+ hookified.prependOnceHook('before:save', async (data) => {});
1508
+ hookified.removeHook('before:save', handler);
1509
+ ```
1510
+
1511
+ **After (v2):**
1512
+
1513
+ ```typescript
1514
+ hookified.onceHook({ event: 'before:save', handler: async (data) => {} });
1515
+ hookified.prependHook({ event: 'before:save', handler: async (data) => {} });
1516
+ hookified.prependOnceHook({ event: 'before:save', handler: async (data) => {} });
1517
+ hookified.removeHook({ event: 'before:save', handler });
1518
+ ```
1519
+
1520
+ ### `onHook` now returns the stored hook
1521
+
1522
+ `onHook` now returns the stored `IHook` object (or `undefined` if blocked by deprecation). Previously it returned `void`. The returned reference is the exact object stored internally, making it easy to later remove with `removeHook()`.
1523
+
1524
+ **Before (v1):**
1525
+
1526
+ ```typescript
1527
+ hookified.onHook({ event: 'before:save', handler }); // void
1528
+ ```
1529
+
1530
+ **After (v2):**
1531
+
1532
+ ```typescript
1533
+ const stored = hookified.onHook({ event: 'before:save', handler }); // IHook | undefined
1534
+ hookified.removeHook(stored); // exact reference match
1535
+ ```
1536
+
1537
+ ## New Features
1538
+
1539
+ ### `Hook` class
1540
+
1541
+ A new `Hook` class is available for creating hook entries. It implements the `IHook` interface and can be used anywhere `IHook` is accepted.
1542
+
1543
+ ```typescript
1544
+ import { Hook } from 'hookified';
1545
+
1546
+ const hook = new Hook('before:save', async (data) => {
1547
+ data.validated = true;
1548
+ });
1549
+
1550
+ myClass.onHook(hook);
1551
+ ```
1552
+
1553
+ ### `WaterfallHook` class
1554
+
1555
+ A new `WaterfallHook` class is available for creating sequential data transformation pipelines. It implements the `IHook` interface and integrates directly with `Hookified.onHook()`. Each hook in the chain receives a `WaterfallHookContext` with `initialArgs` (the original arguments) and `results` (an array of `{ hook, result }` entries from all previous hooks).
1556
+
1557
+ ```typescript
1558
+ import { Hookified, WaterfallHook } from 'hookified';
1559
+
1560
+ class MyClass extends Hookified {
1561
+ constructor() { super(); }
1562
+ }
1563
+
1564
+ const myClass = new MyClass();
1565
+
1566
+ const wh = new WaterfallHook('save', ({ results }) => {
1567
+ const data = results[results.length - 1].result;
1568
+ console.log('Saved:', data);
1569
+ });
1570
+
1571
+ wh.addHook(({ initialArgs }) => {
1572
+ return { ...initialArgs, validated: true };
1573
+ });
1574
+
1575
+ wh.addHook(({ results }) => {
1576
+ return { ...results[results.length - 1].result, timestamp: Date.now() };
1577
+ });
1578
+
1579
+ myClass.onHook(wh);
1580
+ await myClass.hook('save', { name: 'test' });
1581
+ // Saved: { name: 'test', validated: true, timestamp: ... }
1582
+ ```
1583
+
1584
+ See the [Waterfall Hook](#waterfallhook) section for full documentation.
1585
+
1586
+ ### `useHookClone` option
1587
+
1588
+ A new `useHookClone` option (default `true`) controls whether hook objects are shallow-cloned before storing. When enabled, external mutation of a registered hook object won't affect the internal state. Set to `false` to store the original reference for performance or when you need reference equality.
1589
+
1590
+ ```typescript
1591
+ class MyClass extends Hookified {
1592
+ constructor() { super({ useHookClone: false }); }
1593
+ }
1594
+ ```
1595
+
1596
+ ### `onHook` now accepts `OnHookOptions`
1597
+
1598
+ `onHook` now accepts an optional second parameter of type `OnHookOptions`. This allows you to override the instance-level `useHookClone` setting and control hook positioning on a per-call basis.
1599
+
1600
+ ```typescript
1601
+ // Override useHookClone for this specific call
1602
+ hookified.onHook({ event: 'before:save', handler }, { useHookClone: false });
1603
+
1604
+ // Insert at the top of the handlers array instead of the end
1605
+ hookified.onHook({ event: 'before:save', handler }, { position: 'Top' });
1606
+
1607
+ // Insert at a specific index
1608
+ hookified.onHook({ event: 'before:save', handler }, { position: 1 });
1609
+ ```
1610
+
1611
+ ### `IHook` now has an `id` property
1612
+
1613
+ Every hook now has an optional `id` property. If not provided, a UUID is auto-generated via `crypto.randomUUID()`. The `id` enables easier lookups and removal via the new `getHook(id)` and `removeHookById(id)` methods, which search across all events.
1614
+
1615
+ Registering a hook with the same `id` on the same event replaces the existing hook in-place (preserving its position).
1616
+
1617
+ ```typescript
1618
+ // With custom id
1619
+ const stored = hookified.onHook({
1620
+ id: 'my-validation',
1621
+ event: 'before:save',
1622
+ handler: async (data) => { data.validated = true; },
1623
+ });
1624
+
1625
+ // Without id — auto-generated
1626
+ const stored2 = hookified.onHook({
1627
+ event: 'before:save',
1628
+ handler: async (data) => {},
1629
+ });
1630
+ console.log(stored2.id); // e.g. '550e8400-e29b-41d4-a716-446655440000'
1631
+
1632
+ // Look up by id (searches all events)
1633
+ const hook = hookified.getHook('my-validation');
1634
+
1635
+ // Remove by id (searches all events)
1636
+ hookified.removeHookById('my-validation');
1637
+
1638
+ // Remove multiple by ids
1639
+ hookified.removeHookById(['hook-a', 'hook-b']);
1640
+ ```
1641
+
1642
+ The `Hook` class also accepts an optional `id` parameter:
1643
+
1644
+ ```typescript
1645
+ const hook = new Hook('before:save', handler, 'my-custom-id');
1646
+ ```
1647
+
1648
+ ### `removeEventHooks` method
1649
+
1650
+ A new `removeEventHooks(event)` method removes all hooks for a specific event and returns the removed hooks as an `IHook[]` array.
1651
+
1652
+ ```typescript
1653
+ const removed = hookified.removeEventHooks('before:save');
1654
+ console.log(removed.length); // number of hooks removed
1655
+ ```
1656
+
1657
+ # How to Contribute
1658
+
1659
+ Hookified is written in TypeScript and tests are written with `vitest`. To setup the environment and run the tests:
1405
1660
 
1406
1661
  ```bash
1407
1662
  pnpm i && pnpm test
@@ -1418,7 +1673,3 @@ To contribute follow the [Contributing Guidelines](CONTRIBUTING.md) and [Code of
1418
1673
  # License and Copyright
1419
1674
 
1420
1675
  [MIT & © Jared Wray](LICENSE)
1421
-
1422
-
1423
-
1424
-