first commit
This commit is contained in:
104
docs/api-stack/api/api-plugin/src/hook-executor/hook-executor.md
Normal file
104
docs/api-stack/api/api-plugin/src/hook-executor/hook-executor.md
Normal 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.
|
||||
Reference in New Issue
Block a user