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

120 lines
7.7 KiB
Markdown

# @suaveplan/api-plugin - Plugin system foundation for the Suaveplan API — typed PluginDescriptor factory, PluginRegistry with 4-phase lifecycle (onRegister → onInit → onReady / onClose in reverse), deterministic Kahn topological sort with alphabetical tie-break, AbortSignal-enforced initTimeout/shutdownTimeout, type-safe ServiceRegistry via module augmentation, priority-ordered HookExecutor for non-lifecycle hooks, defineCodes-typed PluginError with ten PLUGIN.* codes. Universal — runs unchanged in Bun, Node, and the browser.
Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser.
<!-- begin:auto-docs -->
## Documentation tree
- [context](./src/context/context.md) — The `context` module ships the runtime object every plugin lifecycle hook receives. `PluginContext<TConfig>` carries th…
- [context](./src/context/context.md) — The `context` module ships the runtime object every plugin lifecycle hook receives. `PluginContext<TConfig>` carries th…
- [define](./src/define/define.md) — The `define` module exports a single function — `definePlugin<TConfig>(descriptor)` — that is the canonical entry point…
- [define](./src/define/define.md) — The `define` module exports a single function — `definePlugin<TConfig>(descriptor)` — that is the canonical entry point…
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the single source of truth for every failure mode the `@suaveplan/api-plugin` package can r…
- [error-codes](./src/error-codes/error-codes.md) — The `error-codes` module is the single source of truth for every failure mode the `@suaveplan/api-plugin` package can r…
- [hook-executor](./src/hook-executor/hook-executor.md) — The `hook-executor` module ships a small, priority-ordered dispatcher for **non-lifecycle** plugin hooks — custom event…
- [hook-executor](./src/hook-executor/hook-executor.md) — The `hook-executor` module ships a small, priority-ordered dispatcher for **non-lifecycle** plugin hooks — custom event…
- [registry](./src/registry/registry.md) — The `registry` module owns `PluginRegistry`, the single canonical orchestrator for the plugin system. Its job is to:
- [registry](./src/registry/registry.md) — The `registry` module owns `PluginRegistry`, the single canonical orchestrator for the plugin system. Its job is to:
- [service-registry](./src/service-registry/service-registry.md) — The `service-registry` module defines the single shared interface that plugins augment to publish typed services to oth…
- [service-registry](./src/service-registry/service-registry.md) — The `service-registry` module defines the single shared interface that plugins augment to publish typed services to oth…
- [topo-sort](./src/topo-sort/topo-sort.md) — The `topo-sort` module owns the deterministic dependency-ordering primitive the `PluginRegistry` uses to schedule lifec…
- [topo-sort](./src/topo-sort/topo-sort.md) — The `topo-sort` module owns the deterministic dependency-ordering primitive the `PluginRegistry` uses to schedule lifec…
- [types](./src/types/api-plugin.md) — The `types` module holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…
- [api-plugin](./src/types/api-plugin.md) — The `types` module holds every shared shape used across the api-plugin package: the immutable `PluginDescriptor<TConfig…
<!-- end:auto-docs -->
## What you get
- `definePlugin<TConfig>(descriptor)` — typed plugin-descriptor factory.
- `PluginRegistry` — 4-phase lifecycle (`onRegister → onInit → onReady` forward, `onClose` reverse) with deterministic dep ordering, `initTimeout` / `shutdownTimeout` enforcement via `AbortSignal`, OTel span per hook.
- `topologicalSort` — Kahn's algorithm with alphabetical tie-break + DFS cycle detection.
- `ServiceRegistry` — empty interface designed for **module augmentation**. Plugins declare typed service slots; consumers get fully-typed `getService<K>` / `registerService<K>` with no casts.
- `HookExecutor` — priority-ordered, non-lifecycle hook dispatcher (custom + sibling-package request/tenant/db hooks). Rejects registration of the four lifecycle hook names — those are registry-owned.
- `PluginError` + `PLUGIN_CODES` — 10 stable error codes via `defineCodes`.
## Quick-start
```ts
import { createPluginRegistry, definePlugin } from "@suaveplan/api-plugin";
import z from "zod";
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { db: { query: (sql: string) => unknown } }
}
const db = definePlugin({
name: "db",
version: "0.1.0",
onInit(ctx) {
ctx.registerService("db", { query: (sql) => ({ sql }) });
},
});
const api = definePlugin<{ port: number }>({
name: "api",
version: "0.1.0",
dependencies: ["db"],
configSchema: z.object({ port: z.number().int().positive() }),
defaults: { port: 3000 },
async onReady(ctx) {
const { port } = ctx.getOwnConfig();
const db = ctx.getService("db");
ctx.logger.info("api ready", { port, hasDb: !!db });
},
});
const registry = createPluginRegistry({ initTimeout: 5_000 });
registry.registerAll([db, api]);
await registry.initialize();
// later …
await registry.shutdown();
```
## Modules
| Subpath | Exports |
|---|---|
| `@suaveplan/api-plugin` | barrel — everything below |
| `@suaveplan/api-plugin/define` | `definePlugin` |
| `@suaveplan/api-plugin/registry` | `PluginRegistry`, `createPluginRegistry`, `PluginRegistryOptions` |
| `@suaveplan/api-plugin/context` | `createPluginContext`, `PluginContext` |
| `@suaveplan/api-plugin/service-registry` | `ServiceRegistry` (augment me), `ServiceKey` |
| `@suaveplan/api-plugin/hook-executor` | `HookExecutor`, `createHookExecutor`, `LIFECYCLE_HOOK_NAMES` |
| `@suaveplan/api-plugin/topo-sort` | `topologicalSort`, `validateDependencies` |
| `@suaveplan/api-plugin/error-codes` | `PLUGIN_CODES`, `PluginError`, `PluginErrorCode` |
| `@suaveplan/api-plugin/types` | `PluginDescriptor`, `PluginInstance`, `PluginNode`, `PluginState`, `PluginLifecycleHook`, `TopologicalSortResult` |
## Augmenting `ServiceRegistry`
`ServiceRegistry` ships **empty**. Plugins augment it to publish typed services:
```ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry {
cache: CacheAdapter;
auth: AuthAdapter;
}
}
```
After that:
```ts
ctx.getService("cache"); // CacheAdapter | undefined
ctx.getService("nope"); // type error — not augmented
```
## Error codes
`PLUGIN_CODES` defines ten codes (`PLUGIN.NOT_FOUND`, `PLUGIN.ALREADY_REGISTERED`, `PLUGIN.INIT_FAILED`, `PLUGIN.DEP_MISSING`, `PLUGIN.CIRCULAR_DEPENDENCY`, `PLUGIN.INIT_TIMEOUT`, `PLUGIN.SHUTDOWN_FAILED`, `PLUGIN.INVALID_CONFIG`, `PLUGIN.HOOK_FAILED`, `PLUGIN.ABORTED`). Every code is raised by at least one production call site — the spec-conformance test pins this invariant.
## design decisions
- **Unified lifecycle runtime** — `PluginRegistry` calls `onRegister`/`onInit`/`onReady`/`onClose` directly; the `HookExecutor` rejects registration of these four names. Upstream's dual-runtime hazard is eliminated by construction.
- **`initTimeout`/`shutdownTimeout` are enforced** — every hook runs with an `AbortController`-bound `ctx.signal` that fires when the budget is exceeded.
- **`getService<K>` is type-safe** via `ServiceRegistry` module augmentation. No more `getService<MyType>(name)` casts.
- **Topological sort is deterministic** — alphabetical tie-break for equal-in-degree nodes makes boot logs reproducible across runs.
- **No `applyMigrations`** — migrations live in `@suaveplan/api-migrations`. The plugin registry knows nothing about databases.
- **No ALS singletons** (`getCurrentApp`, etc.). Callers thread `ctx` explicitly.