Skip to content

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 into App<TCaps> via structural intersection.

Signature

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

FieldWhen calledPurpose
nameAt registrationStable identifier for diagnostics and duplicate-install detection
provideCtx?({envelope, container, runtime})Per dispatchReturns the object spread into handler ctx
provideRuntime?(runtime)Once at installReturns 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:

ts
import { definePlugin } from "@nwire/app";

export function clockPlugin() {
  return definePlugin<ClockCaps>("clock", {
    setup: ({ add }) => {
      add(clockCap);
    },
  });
}

Then layer onto an App:

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

ts
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 call

Type accumulation

The phantom marker is how App<TCaps> accumulates contributions:

ts
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

MIT licensed.