Skip to main content

Run one-shot and parallel hooks

When building complex systems, you often need to trigger logic that should only run once—such as an initial setup routine—or execute multiple independent tasks simultaneously to improve performance. If you use standard serial hooks for these scenarios, you might end up with redundant executions or unnecessary delays.

One-Shot Execution with hookOnce

The hookOnce method allows you to register a callback that automatically unregisters itself after its first execution. This is useful for initialization logic or "first-run" events where subsequent triggers of the same hook should be ignored by that specific handler.

Internally, hookOnce wraps your callback in a proxy function. When the hook is called, this proxy invokes the unregistration function returned by the underlying hook call before executing your original logic. This ensures that even if the hook is triggered multiple times, your handler only runs once.

import { createHooks } from 'hookable';

interface MyHooks {
'init:config': (version: string) => void;
}

async function runOneShotExample() {
const hooks = createHooks<MyHooks>();
let callCount = 0;

// Register a one-shot hook
hooks.hookOnce('init:config', (version) => {
callCount++;
console.log(`Initializing with version: ${version}`);
});

// First call: handler runs
await hooks.callHook('init:config', '1.0.0');

// Second call: handler is already unregistered and will not run
await hooks.callHook('init:config', '1.0.0');

console.log(`Total executions: ${callCount}`); // Total executions: 1
}

runOneShotExample();

Parallel Execution and Manual Removal

While callHook executes handlers sequentially (awaiting each one before moving to the next), callHookParallel triggers all registered handlers concurrently using Promise.all. This is ideal for independent tasks like logging, analytics, or background syncs where the order of completion does not matter.

If you need to stop a handler from responding to future events, use removeHook. This method requires a reference to the original function. If you register the same function multiple times, removeHook removes only one instance per call by searching the internal _hooks array for the function's index and splicing it out.

import { createHooks } from 'hookable';

interface MyHooks {
'data:sync': (id: string) => Promise<void>;
}

async function runParallelExample() {
const hooks = createHooks<MyHooks>();

const syncToCloud = async (id: string) => {
await new Promise(resolve => setTimeout(resolve, 50));
console.log(`Synced ${id} to cloud`);
};

const logActivity = async (id: string) => {
console.log(`Activity logged for ${id}`);
};

// Register named handlers
hooks.hook('data:sync', syncToCloud);
hooks.hook('data:sync', logActivity);

// Dispatch all handlers in parallel
await hooks.callHookParallel('data:sync', 'item-123');

// Remove a specific handler so it doesn't run on the next call
hooks.removeHook('data:sync', syncToCloud);

// Only logActivity will run now
await hooks.callHookParallel('data:sync', 'item-456');
}

runParallelExample();

The Hookable class manages these registrations in the _hooks object, where each key is a hook name mapping to an array of HookCallback functions. When you call removeHook, the class cleans up the array and deletes the key entirely if no handlers remain, preventing memory leaks from empty hook definitions.