first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# hook-executor
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.