Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# context
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `context` module ships the runtime object every plugin lifecycle
hook receives. `PluginContext<TConfig>` carries the structural `logger`
and `eventBus` (typed against the universal contracts in
`@suaveplan/types`), the in-flight `signal` (an `AbortSignal` the
registry aborts on timeout or shutdown), the plugin's own merged config
via `getOwnConfig()`, and the typed `registerService`/`getService`/
`hasService` trio backed by the shared `ServiceRegistry` augmentation
surface.
`createPluginContext` is the constructor — the registry calls it once
per plugin per lifecycle phase, binding that plugin's config and the
shared `services` Map. The Map instance is shared across every plugin
in the same boot, so a producer plugin's `registerService` is visible
to every later consumer's `getService`.
## Features
- Universal — no `node:fs`, no DOM, no Hono dependency. Works in
Bun/Node/browser.
- Typed `getService<K>` / `registerService<K>` via the augmentable
`ServiceRegistry` interface; untyped string keys fail at compile time.
- `signal: AbortSignal` for graceful cooperative cancellation — every
lifecycle hook can branch on `ctx.signal.aborted` or thread the signal
into long-running async work.
- `getOwnConfig()` returns the calling plugin's `TConfig` with full
schema inference from `definePlugin`.
- Stateless — no internal closures keep references after the hook
returns; the `services` Map lifetime is owned by the registry.
- No `getPluginConfig(name)` cross-plugin coupling (unlike upstream).
Reading another plugin's config goes through a typed service.
## Usage: Basic
A plugin hook reading its own config and registering a service:
```ts
import { definePlugin } from "@suaveplan/api-plugin";
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { http: { port: number } }
}
export const httpPlugin = definePlugin<{ port: number }>({
name: "http",
version: "0.1.0",
defaults: { port: 3000 },
onInit(ctx) {
const cfg = ctx.getOwnConfig();
ctx.registerService("http", { port: cfg.port });
},
});
```
## Usage: Advanced
A consumer plugin observing the abort signal during async setup:
```ts
export const slowPlugin = definePlugin({
name: "slow",
version: "0.1.0",
dependencies: ["http"],
async onReady(ctx) {
const http = ctx.getService("http");
if (!http) return;
ctx.logger.info("warming up");
await new Promise<void>((resolve, reject) => {
const onAbort = () => reject(new Error("aborted"));
ctx.signal.addEventListener("abort", onAbort, { once: true });
setTimeout(() => {
ctx.signal.removeEventListener("abort", onAbort);
resolve();
}, 50);
});
},
});
```
## API Reference
| Export | Signature |
|---|---|
| `createPluginContext<TConfig>(options)` | `(CreatePluginContextOptions<TConfig>) => PluginContext<TConfig>` |
| `CreatePluginContextOptions<TConfig>` | `{ logger, eventBus, signal, config, services }` |
| `PluginContext<TConfig>` | `{ logger, eventBus, signal, getOwnConfig(), registerService<K>, getService<K>, hasService }` |
## Implementation Notes
The shared `services` Map is intentionally not snapshotted or copied
between phases — that would defeat the producer/consumer model. The
registry passes the same reference into every call to
`createPluginContext`, so registration order across phases is
preserved.
`registerService` casts the `K` parameter to `string` once when writing
to the Map; everywhere else the API is typed against
`ServiceRegistry[K]`. Because consumers augment via `declare module`,
the cast is the only place the runtime "knows" services are
string-keyed — every public site is typed.
`signal` is read-only on the context; the registry holds the
`AbortController` and decides when to abort (timeout / shutdown). Hooks
must never construct their own signal and feed it back into the context.