Files
skills/docs/api-stack/api/api-plugin/src/hook-executor/hook-executor.md
2026-05-29 08:20:56 +01:00

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 — 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

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.