4.0 KiB
context
(no submodules)
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 augmentableServiceRegistryinterface; untyped string keys fail at compile time. signal: AbortSignalfor graceful cooperative cancellation — every lifecycle hook can branch onctx.signal.abortedor thread the signal into long-running async work.getOwnConfig()returns the calling plugin'sTConfigwith full schema inference fromdefinePlugin.- Stateless — no internal closures keep references after the hook
returns; the
servicesMap 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:
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:
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.