defineCapability
Typed slice of handler ctx. The primitive every plugin uses to contribute to dispatch without the App layer naming the contribution directly.
A capability declares two things:
provideCtx({envelope, container, runtime})— what the handler ctx gets at dispatch time.__mark: TMark— phantom type carrying the ctx-shape intoApp<TCaps>via structural intersection.
Signature
import { defineCapability, type Capability } from "@nwire/runtime";
interface ClockCaps {
now(): Date;
}
const clockCap: Capability<ClockCaps> = defineCapability<ClockCaps>({
name: "clock",
provideCtx: () => ({
now: () => new Date(),
}),
});Three fields on the Capability<TMark> shape:
| Field | When called | Purpose |
|---|---|---|
name | At registration | Stable identifier for diagnostics and duplicate-install detection |
provideCtx?({envelope, container, runtime}) | Per dispatch | Returns the object spread into handler ctx |
provideRuntime?(runtime) | Once at install | Returns methods to attach directly to runtime |
provideCtx is called once per dispatch, not once per process. Closure state lives per-dispatch.
Installing via a plugin
Capabilities don't install themselves. Wrap with definePlugin:
import { definePlugin } from "@nwire/app";
export function clockPlugin() {
return definePlugin<ClockCaps>("clock", {
setup: ({ add }) => {
add(clockCap);
},
});
}Then layer onto an App:
const app = createApp({ appName: "shop" })
.with(clockPlugin())
.with(idPlugin());
// Handler ctx narrows to ClockCaps & IdCaps.
async function stampHandler(input: { label: string }, ctx: ClockCaps & IdCaps) {
return {
id: ctx.newId(),
label: input.label,
at: ctx.now().toISOString(),
};
}
await app.runtime.execute(stampHandler, { label: "hello" });Runtime-level methods
provideRuntime attaches methods directly to runtime. Useful for diagnostics and scheduler-style helpers that don't fit a dispatch:
defineCapability<{ now(): Date }>({
name: "clock",
provideCtx: () => ({ now: () => new Date() }),
provideRuntime: () => ({
now: () => new Date(),
}),
});
// After install, both shapes are available:
await app.runtime.execute(handler, input); // ctx.now() inside handler
const t = app.runtime.now(); // direct callType accumulation
The phantom marker is how App<TCaps> accumulates contributions:
const app = createApp({ appName: "x" })
.with(clockPlugin()) // App<{} & ClockCaps>
.with(idPlugin()) // App<ClockCaps & IdCaps>
.with(forgePlugin); // App<ClockCaps & IdCaps & ForgeCaps>A handler that asks for caps the App doesn't carry won't compile — that's the type-side benefit. The runtime side runs whatever's installed regardless of types.
See also
- Capability concept
- definePlugin
- The
capability-compositionexample for a runnable demo.