Files
2026-05-27 23:29:53 +01:00

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 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:

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.