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 +958 -707
- package/dist/browser/index.global.js +283 -155
- package/dist/browser/index.global.js.map +1 -1
- package/dist/browser/index.js +286 -156
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +289 -157
- package/dist/node/index.d.cts +206 -79
- package/dist/node/index.d.ts +206 -79
- package/dist/node/index.js +286 -156
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/jaredwray/hookified/actions/workflows/tests.yaml)
|
|
6
6
|
[](https://github.com/jaredwray/hookified/blob/master/LICENSE)
|
|
7
|
-
[](https://codecov.io/gh/jaredwray/hookified)
|
|
7
|
+
[](https://codecov.io/gh/jaredwray/hookified)
|
|
8
8
|
[](https://npmjs.com/package/hookified)
|
|
9
9
|
[](https://www.jsdelivr.com/package/npm/hookified)
|
|
10
10
|
[](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
|
-
-
|
|
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
|
-
- [.
|
|
37
|
-
- [.
|
|
38
|
-
- [.
|
|
39
|
-
- [.
|
|
40
|
-
- [.
|
|
41
|
-
- [.
|
|
42
|
-
- [.
|
|
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
|
-
- [.
|
|
50
|
-
- [.
|
|
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
|
-
- [.
|
|
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');
|
|
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');
|
|
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');
|
|
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
|
-
#
|
|
186
|
+
# Hooks
|
|
179
187
|
|
|
180
|
-
##
|
|
188
|
+
## Standard Hook
|
|
181
189
|
|
|
182
|
-
|
|
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
|
-
|
|
213
|
+
// Without id (auto-generated)
|
|
214
|
+
const hook = new Hook('before:save', async (data) => {
|
|
215
|
+
data.validated = true;
|
|
216
|
+
});
|
|
196
217
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
console.log(error.message); // error
|
|
205
|
-
}
|
|
223
|
+
// Register with onHook
|
|
224
|
+
myClass.onHook(hook);
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
233
|
+
// Remove hooks
|
|
234
|
+
myClass.removeHooks(hooks);
|
|
235
|
+
```
|
|
213
236
|
|
|
214
|
-
|
|
215
|
-
import { Hookified } from 'hookified';
|
|
216
|
-
import pino from 'pino';
|
|
237
|
+
**Using plain TypeScript with the `IHook` interface:**
|
|
217
238
|
|
|
218
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
##
|
|
263
|
+
## Waterfall Hook
|
|
244
264
|
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
import { Hookified } from 'hookified';
|
|
267
|
+
The `WaterfallHookContext` has the following properties:
|
|
249
268
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
274
|
+
**Basic usage:**
|
|
257
275
|
|
|
258
|
-
|
|
276
|
+
```javascript
|
|
277
|
+
import { WaterfallHook } from 'hookified';
|
|
259
278
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
285
|
+
// Add transformation hooks to the pipeline
|
|
286
|
+
wh.addHook(({ initialArgs }) => {
|
|
287
|
+
return initialArgs + 1; // 5 -> 6
|
|
267
288
|
});
|
|
268
289
|
|
|
269
|
-
|
|
270
|
-
|
|
290
|
+
wh.addHook(({ results }) => {
|
|
291
|
+
return results[results.length - 1].result * 2; // 6 -> 12
|
|
271
292
|
});
|
|
272
293
|
|
|
273
|
-
//
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
//
|
|
341
|
-
myClass.
|
|
322
|
+
// Register with Hookified — works because WaterfallHook implements IHook
|
|
323
|
+
myClass.onHook(wh);
|
|
342
324
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
deprecatedHooks,
|
|
349
|
-
logger
|
|
350
|
-
});
|
|
330
|
+
**Managing hooks:**
|
|
351
331
|
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
- Registration: `onHook()`, `addHook()`, `onHookEntry()`, `onHooks()`, `prependHook()`, `onceHook()`, `prependOnceHook()`
|
|
357
|
-
- Execution: `hook()`, `callHook()`
|
|
358
|
-
- Management: `getHooks()`, `removeHook()`, `removeHooks()`
|
|
345
|
+
# API - Hooks
|
|
359
346
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
- **
|
|
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
|
-
## .
|
|
426
|
+
## .deprecatedHooks
|
|
435
427
|
|
|
436
|
-
|
|
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
|
-
|
|
448
|
+
console.log(myClass.deprecatedHooks); // Map with deprecated hooks
|
|
462
449
|
|
|
463
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
472
|
+
// Works with logger if provided
|
|
473
|
+
import pino from 'pino';
|
|
474
|
+
const logger = pino();
|
|
481
475
|
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
## .
|
|
494
|
+
## .enforceBeforeAfter
|
|
496
495
|
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
+
console.log(myClass.enforceBeforeAfter); // true
|
|
515
510
|
|
|
516
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
552
|
+
import pino from 'pino';
|
|
543
553
|
|
|
544
|
-
|
|
545
|
-
constructor() {
|
|
546
|
-
super();
|
|
547
|
-
}
|
|
554
|
+
const myClass = new MyClass({ eventLogger: pino() });
|
|
548
555
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
await this.hook('before:myMethod2', data);
|
|
556
|
+
myClass.onHook({ event: 'before:myMethod2', handler: async () => {
|
|
557
|
+
throw new Error('error');
|
|
558
|
+
}});
|
|
553
559
|
|
|
554
|
-
|
|
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
|
-
|
|
564
|
+
## .hooks
|
|
559
565
|
|
|
560
|
-
|
|
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
|
-
|
|
568
|
+
```javascript
|
|
569
|
+
myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
|
|
570
|
+
data.some = 'new data';
|
|
571
|
+
}});
|
|
565
572
|
|
|
566
|
-
console.log(myClass.hooks
|
|
573
|
+
console.log(myClass.hooks); // Map { 'before:myMethod2' => [{ event: 'before:myMethod2', handler: [Function] }] }
|
|
567
574
|
```
|
|
568
575
|
|
|
569
|
-
## .
|
|
576
|
+
## .throwOnHookError
|
|
570
577
|
|
|
571
|
-
|
|
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
|
-
|
|
581
|
+
const myClass = new MyClass({ throwOnHookError: true });
|
|
575
582
|
|
|
576
|
-
|
|
577
|
-
constructor() {
|
|
578
|
-
super();
|
|
579
|
-
}
|
|
583
|
+
console.log(myClass.throwOnHookError); // true
|
|
580
584
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
+
try {
|
|
586
|
+
myClass.onHook({ event: 'error-event', handler: async () => {
|
|
587
|
+
throw new Error('error');
|
|
588
|
+
}});
|
|
585
589
|
|
|
586
|
-
|
|
587
|
-
|
|
590
|
+
await myClass.hook('error-event');
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.log(error.message); // error
|
|
588
593
|
}
|
|
589
594
|
|
|
590
|
-
|
|
591
|
-
myClass.
|
|
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
|
-
## .
|
|
599
|
+
## .useHookClone
|
|
600
600
|
|
|
601
|
-
|
|
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
|
-
|
|
604
|
+
const myClass = new MyClass({ useHookClone: false });
|
|
605
605
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
super();
|
|
609
|
-
}
|
|
606
|
+
const hook = { event: 'before:save', handler: async (data) => {} };
|
|
607
|
+
myClass.onHook(hook);
|
|
610
608
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
613
|
+
// You can dynamically change the setting
|
|
614
|
+
myClass.useHookClone = true;
|
|
615
|
+
```
|
|
619
616
|
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
## .
|
|
627
|
+
## .afterHook(eventName, ...args)
|
|
630
628
|
|
|
631
|
-
|
|
629
|
+
This is a helper function that will prepend a hook name with `after:`.
|
|
632
630
|
|
|
633
631
|
```javascript
|
|
634
|
-
|
|
632
|
+
// Inside your class method — the event name will be `after:myMethod2`
|
|
633
|
+
await this.afterHook('myMethod2', data);
|
|
634
|
+
```
|
|
635
635
|
|
|
636
|
-
|
|
637
|
-
constructor() {
|
|
638
|
-
super();
|
|
639
|
-
}
|
|
636
|
+
## .beforeHook(eventName, ...args)
|
|
640
637
|
|
|
641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
const handler = async (data) => {
|
|
652
|
-
data.some = 'new data';
|
|
653
|
-
};
|
|
645
|
+
## .callHook(eventName, ...args)
|
|
654
646
|
|
|
655
|
-
|
|
647
|
+
This is an alias for `.hook(eventName, ...args)` for backwards compatibility.
|
|
656
648
|
|
|
657
|
-
|
|
658
|
-
```
|
|
649
|
+
## .clearHooks()
|
|
659
650
|
|
|
660
|
-
|
|
661
|
-
Unsubscribe from multiple hooks.
|
|
651
|
+
Clear all hooks across all events.
|
|
662
652
|
|
|
663
653
|
```javascript
|
|
664
|
-
|
|
654
|
+
myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
|
|
655
|
+
data.some = 'new data';
|
|
656
|
+
}});
|
|
665
657
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
super();
|
|
669
|
-
}
|
|
658
|
+
myClass.clearHooks();
|
|
659
|
+
```
|
|
670
660
|
|
|
671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
702
|
-
|
|
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
|
-
## .
|
|
680
|
+
## .getHooks(eventName)
|
|
706
681
|
|
|
707
|
-
|
|
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
|
-
|
|
685
|
+
myClass.onHook({ event: 'before:myMethod2', handler: async (data) => {
|
|
686
|
+
data.some = 'new data';
|
|
687
|
+
}});
|
|
711
688
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
super();
|
|
715
|
-
}
|
|
689
|
+
console.log(myClass.getHooks('before:myMethod2')); // [{ event: 'before:myMethod2', handler: [Function] }]
|
|
690
|
+
```
|
|
716
691
|
|
|
717
|
-
|
|
718
|
-
let data = { some: 'data' };
|
|
719
|
-
// do something
|
|
720
|
-
await this.hook('before:myMethod2', data);
|
|
692
|
+
## .hook(eventName, ...args)
|
|
721
693
|
|
|
722
|
-
|
|
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
|
-
|
|
701
|
+
You can pass multiple arguments to the hook:
|
|
728
702
|
|
|
729
703
|
```javascript
|
|
730
|
-
|
|
704
|
+
// Inside your class method
|
|
705
|
+
await this.hook('before:myMethod2', data, data2);
|
|
731
706
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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
|
-
## .
|
|
735
|
+
## .onHook(hook, options?)
|
|
758
736
|
|
|
759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
+
// Remove by id
|
|
771
|
+
myClass.removeHookById('my-validation');
|
|
786
772
|
|
|
787
|
-
|
|
788
|
-
|
|
773
|
+
// Use the returned reference to remove the hook later
|
|
774
|
+
myClass.removeHook(stored);
|
|
789
775
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
784
|
+
// Insert at a specific index
|
|
785
|
+
myClass.onHook({ event: 'before:save', handler: async (data) => {} }, { position: 1 });
|
|
803
786
|
```
|
|
804
787
|
|
|
805
|
-
## .
|
|
788
|
+
## .onHooks(Array, options?)
|
|
806
789
|
|
|
807
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
812
|
+
// With options — skip cloning for all hooks in this batch
|
|
813
|
+
myClass.onHooks(hooks, { useHookClone: false });
|
|
814
|
+
```
|
|
827
815
|
|
|
828
|
-
|
|
816
|
+
## .onceHook(hook)
|
|
829
817
|
|
|
830
|
-
|
|
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
|
-
|
|
836
|
-
myClass.
|
|
837
|
-
data.some = '
|
|
838
|
-
});
|
|
820
|
+
```javascript
|
|
821
|
+
myClass.onceHook({ event: 'before:myMethod2', handler: async (data) => {
|
|
822
|
+
data.some = 'new data';
|
|
823
|
+
}});
|
|
839
824
|
|
|
840
|
-
myClass.
|
|
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
|
-
## .
|
|
829
|
+
## .prependHook(hook, options?)
|
|
844
830
|
|
|
845
|
-
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
constructor() {
|
|
852
|
-
super();
|
|
853
|
-
}
|
|
845
|
+
## .prependOnceHook(hook, options?)
|
|
854
846
|
|
|
855
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
849
|
+
An optional `PrependHookOptions` object can be passed with:
|
|
850
|
+
- `useHookClone` (boolean) — per-call override for hook cloning behavior
|
|
863
851
|
|
|
864
|
-
|
|
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
|
-
|
|
855
|
+
}});
|
|
856
|
+
myClass.prependOnceHook({ event: 'before:myMethod2', handler: async (data) => {
|
|
857
|
+
data.some = 'will run before new data';
|
|
858
|
+
}});
|
|
870
859
|
```
|
|
871
860
|
|
|
872
|
-
## .
|
|
861
|
+
## .removeEventHooks(eventName)
|
|
873
862
|
|
|
874
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
885
|
-
let data = { some: 'data' };
|
|
886
|
-
// do something
|
|
887
|
-
await this.hook('before:myMethod2', data);
|
|
878
|
+
## .removeHook(hook)
|
|
888
879
|
|
|
889
|
-
|
|
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
|
-
|
|
894
|
-
|
|
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
|
-
|
|
889
|
+
const removed = myClass.removeHook({ event: 'before:myMethod2', handler });
|
|
890
|
+
console.log(removed); // { event: 'before:myMethod2', handler: [Function] }
|
|
899
891
|
```
|
|
900
892
|
|
|
901
|
-
## .
|
|
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
|
-
|
|
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
|
-
|
|
903
|
+
const myClass = new MyClass();
|
|
907
904
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
+
## .removeHooks(Array)
|
|
923
919
|
|
|
924
|
-
|
|
925
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
class MyClass extends Hookified {
|
|
941
|
-
constructor() {
|
|
942
|
-
super();
|
|
943
|
-
}
|
|
949
|
+
const myClass = new MyClass({ throwOnEmitError: true });
|
|
944
950
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
await this.hook('before:myMethod2', data);
|
|
951
|
+
myClass.on('error', (err) => {
|
|
952
|
+
console.log('listener received:', err.message);
|
|
953
|
+
});
|
|
949
954
|
|
|
950
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1044
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
}); //
|
|
1064
|
+
}); // warning emitted but listener is still added
|
|
1146
1065
|
|
|
1147
|
-
console.log(myClass.listenerCount('message')); //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` | `
|
|
1293
|
-
| `warn` | `
|
|
1294
|
-
| `debug` | `
|
|
1295
|
-
| `trace` | `
|
|
1296
|
-
| `fatal` | `
|
|
1297
|
-
| Any other | `
|
|
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
|
|
1212
|
+
You can also set or change the eventLogger after instantiation:
|
|
1355
1213
|
|
|
1356
1214
|
```javascript
|
|
1357
1215
|
const service = new MyService();
|
|
1358
|
-
service.
|
|
1216
|
+
service.eventLogger = pino({ level: 'debug' });
|
|
1359
1217
|
|
|
1360
|
-
// Or remove the
|
|
1361
|
-
service.
|
|
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
|
|
1371
|
-
|
|
1372
|
-
| Hookified (
|
|
1373
|
-
| Hookable (v6.0.1)
|
|
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
|
-
|
|
|
1382
|
-
|
|
|
1383
|
-
| EventEmitter (
|
|
1384
|
-
| Emittery (v1.2.0) | -
|
|
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
|
-
#
|
|
1246
|
+
# Migrating from v1 to v2
|
|
1389
1247
|
|
|
1390
|
-
|
|
1248
|
+
## Quick Guide
|
|
1391
1249
|
|
|
1392
|
-
|
|
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
|
-
```
|
|
1395
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
1401
|
-
|
|
1319
|
+
```javascript
|
|
1320
|
+
super({ throwHookErrors: true });
|
|
1321
|
+
myClass.throwHookErrors = false;
|
|
1402
1322
|
```
|
|
1403
1323
|
|
|
1404
|
-
|
|
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
|
-
|