3.9 KiB
hook-executor
(no submodules)
Purpose
The hook-executor module ships a small, priority-ordered dispatcher
for non-lifecycle plugin hooks — custom events plugins contribute to
each other, plus the request/tenant/database hook surfaces that ship in
sibling packages (@suaveplan/api, @suaveplan/api-tenant,
@suaveplan/api-migrations). The four lifecycle hooks
(onRegister/onInit/onReady/onClose) are explicitly rejected by
registerHook so the upstream dual-runtime hazard (registry calls a
lifecycle hook directly AND the executor dispatches it) cannot recur.
execute runs handlers sequentially in priority order (higher first)
and aborts on the first throw, wrapping the cause in
PluginError(PLUGIN.HOOK_FAILED) carrying the offending plugin name
and hook name. executeAll runs handlers in parallel via
Promise.allSettled, returning the result vector so callers can
inspect each rejection.
Features
- Priority sort —
registerHookkeeps the list ordered after every insertion, so dispatch is O(n) and stable for equal priorities. - Lifecycle-hook rejection — registering any of the four registry-owned
names throws
PLUGIN.HOOK_FAILEDsynchronously at registration time. unregisterPlugin(name)removes every contribution from a plugin in one call (used byPluginRegistry.disable).- Sequential or parallel dispatch —
executeaborts on first throw,executeAllsettles all and returns the result vector. - Inspection —
hasHook,getHookCount,getRegisteredHooks,clearfor tests and diagnostics.
Usage: Basic
import { createHookExecutor } from "@suaveplan/api-plugin/hook-executor";
const hooks = createHookExecutor();
hooks.registerHook("on-cache-evict", (key) => console.log("evict", key), "cache-plugin", 10);
hooks.registerHook("on-cache-evict", (key) => console.log("audit", key), "audit-plugin", 5);
await hooks.execute("on-cache-evict", "session:42");
// prints "evict session:42" then "audit session:42"
Usage: Advanced
Parallel dispatch with partial failures:
const results = await hooks.executeAll("warm-cache");
for (const result of results) {
if (result.status === "rejected") {
console.error("warm-cache hook failed:", result.reason);
}
}
Attempting to register a lifecycle hook is rejected:
import { PluginError } from "@suaveplan/api-plugin/error-codes";
try {
hooks.registerHook("onInit", () => {}, "bad-plugin");
} catch (e) {
if (e instanceof PluginError) console.log(e.code); // "PLUGIN.HOOK_FAILED"
}
API Reference
| Export | Signature |
|---|---|
HookExecutor |
class — see methods below |
createHookExecutor() |
() => HookExecutor |
LIFECYCLE_HOOK_NAMES |
readonly ["onRegister", "onInit", "onReady", "onClose"] |
.registerHook(name, handler, pluginName, priority?) |
rejects lifecycle names |
.unregisterPlugin(pluginName) |
bulk-remove |
.execute(name, ...args) |
sequential, abort-on-throw |
.executeAll(name, ...args) |
parallel, settle-all |
.hasHook(name) / .getHookCount(name) / .getRegisteredHooks() |
inspection |
.clear() |
reset |
Implementation Notes
The lifecycle-name guard is a single Set.has lookup against a frozen
LIFECYCLE_HOOK_NAMES constant. Both the constant and the type derived
from it (LifecycleHookName) are exported so sibling packages can
re-use the same authoritative list.
execute does not parallelise — handlers may have ordering
dependencies (a higher-priority handler may publish data a lower one
reads). executeAll is the explicit opt-in for fan-out scenarios
(metrics emitters, listeners with no producer ordering).
The wrapped PluginError carries the original throw as cause rather
than concatenating into the message — consumers can walk the cause
chain via walkCauses from @suaveplan/error to surface the inner
diagnostic without parsing strings.