Hookified: Event Emitting and Middleware Hooks
Hookified provides a robust solution for integrating event emitting and middleware-style hooks into JavaScript and TypeScript applications. Currently at version 2.1.1, the library maintains an active release cadence, with frequent updates addressing performance, type improvements, and feature enhancements. Key differentiators include its dual support for asynchronous and synchronous middleware hooks, serving as a simple yet powerful EventEmitter replacement. It offers comprehensive support for ESM and CJS module systems, along with TypeScript types and compatibility with Node.js 20+ environments, including browser support via CDN. The library boasts a lean footprint with no external package dependencies and a small size (around 250KB), focusing on fast and efficient execution, as evidenced by its benchmarks. Features like WaterfallHook for data transformation, robust error handling, options for enforcing hook naming conventions (enforceBeforeAfter), and built-in mechanisms for managing deprecated hooks (deprecatedHooks, allowDeprecated) further distinguish its capabilities.
Common errors
-
TypeError: 'logger' is not a function
cause Attempting to call or access the `logger` property which was renamed to `eventLogger` in Hookified v2.0.0.fixChange references from `myInstance.logger` to `myInstance.eventLogger` or `this.eventLogger`. -
Error: Hook event "myCustomHook" must start with "before" or "after" when enforceBeforeAfter is enabled
cause The `enforceBeforeAfter` option is set to `true`, requiring all hook names to follow a 'before:' or 'after:' prefix convention, and the provided hook name does not.fixRename your hook to `beforeMyCustomHook` or `afterMyCustomHook`, or disable the enforcement by passing `{ enforceBeforeAfter: false }` to the `Hookified` constructor. -
TypeError: Hookified is not a constructor (in CommonJS context)
cause Using `const Hookified = require('hookified');` in a CommonJS module when the library is primarily designed for ESM, leading to module interoperability issues.fixRefactor your module to use ESM `import { Hookified } from 'hookified';` or use dynamic imports `import('hookified').then(module => new module.Hookified());` if you must remain in CJS. -
UnhandledPromiseRejectionWarning: Error: No listeners for event: error
cause An 'error' event was `emit`ted, and the `throwOnEmptyListeners` option (which defaults to `true` in v2+) is enabled, but no listener was registered for the 'error' event.fixAdd an 'error' event listener: `myInstance.on('error', (err) => { console.error('Caught error:', err); });` or set `new Hookified({ throwOnEmptyListeners: false })` to suppress this behavior.
Warnings
- breaking The `logger` property was renamed to `eventLogger` in v2.0.0 to prevent naming conflicts. Code accessing `this.logger` directly will fail.
- breaking The `throwHookErrors` option was removed in v2.0.0. Its functionality is now integrated into `throwOnHookError`.
- breaking The default value for `throwOnEmptyListeners` changed to `true` in v2.0.0. This means emitting an 'error' event without any registered listeners will now throw an exception, aligning with Node.js EventEmitter behavior.
- breaking Version 2.0.0 introduced significant changes to internal types and interfaces, including `Hook` type becoming `HookFn`, `onHook` accepting `IHook` objects, and `hooks` being stored as an array of `IHook`. While direct usage of `Hookified` should mostly be compatible, custom hook implementations or deep integrations might require adjustments.
- gotcha Enabling `enforceBeforeAfter` in constructor options (e.g., `new Hookified({ enforceBeforeAfter: true })`) requires all hook names to explicitly start with 'before' or 'after'. Not adhering to this convention will result in runtime errors.
- gotcha The `.hookSync()` method explicitly skips asynchronous (async/await) handler functions. If you need to execute both synchronous and asynchronous handlers, use the `.hook()` method, which is the preferred and more comprehensive approach.
Install
-
npm install hookified -
yarn add hookified -
pnpm add hookified
Imports
- Hookified
const Hookified = require('hookified');import { Hookified } from 'hookified'; - Hook
import { IHook } from 'hookified'; // IHook is an interface, not a class for direct instantiationimport { Hook } from 'hookified'; - WaterfallHook
import { WaterfallHook } from 'hookified';
Quickstart
import { Hookified, Hook, WaterfallHook } from 'hookified';
class MyService extends Hookified {
constructor() {
super();
this.data = { count: 0, items: [] };
}
async processData(input: string) {
await this.hook('beforeProcess', input);
this.data.count++;
this.data.items.push(input.toUpperCase());
await this.hook('afterProcess', this.data);
return this.data;
}
async saveData(initialData: any) {
const processedData = await this.hook('saveData', initialData);
console.log('Final data saved:', processedData);
this.emit('dataSaved', processedData);
}
}
const service = new MyService();
// Register a standard hook handler for 'beforeProcess'
service.onHook({
event: 'beforeProcess',
handler: async (input: string) => {
console.log(`[Hook: beforeProcess] Input received: ${input}`);
}
});
// Register another hook for 'afterProcess'
service.onHook('afterProcess', (data: { count: number }) => {
console.log(`[Hook: afterProcess] Current count: ${data.count}`);
});
// Register a WaterfallHook for 'saveData' to transform data sequentially
const transformHook = new WaterfallHook('saveData', ({ args, results }) => {
const currentResult = results.length > 0 ? results[results.length - 1].result : args[0];
const transformed = { ...currentResult, timestamp: Date.now(), processedBy: 'transformHook' };
console.log('[WaterfallHook: saveData] Data transformed.');
return transformed;
});
service.onHook(transformHook);
// Register an event listener
service.on('dataSaved', (data) => {
console.log(`[Event: dataSaved] Event emitted with data: ${JSON.stringify(data)}`);
});
// Run the service
(async () => {
console.log('--- Starting processData ---');
await service.processData('hello world');
await service.processData('hookified');
console.log('--- Finished processData ---\n');
console.log('--- Starting saveData ---');
await service.saveData({ id: 1, value: 'initial' });
console.log('--- Finished saveData ---');
// Example of using a hook programmatically (equivalent to onHook but for single use)
await service.onceHook(new Hook('afterProcess', (data) => {
console.log(`[Hook: onceHook] Special one-time afterProcess hook triggered with count: ${data.count}`);
}));
await service.processData('one-time-hook-test');
})();