# 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 — `registerHook` keeps 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_FAILED` synchronously at registration time. - `unregisterPlugin(name)` removes every contribution from a plugin in one call (used by `PluginRegistry.disable`). - Sequential or parallel dispatch — `execute` aborts on first throw, `executeAll` settles all and returns the result vector. - Inspection — `hasHook`, `getHookCount`, `getRegisteredHooks`, `clear` for tests and diagnostics. ## Usage: Basic ```ts 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: ```ts 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: ```ts 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.