@nwire/runtime 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,13 +19,16 @@ import type { Container } from "@nwire/container";
19
19
  import type { Runtime } from "./runtime.js";
20
20
  /**
21
21
  * Base shape passed to `provideCtx`. The capability returns a Record that is
22
- * spread into the handler ctx for one dispatch. `envelope`, `container`, and
23
- * `runtime` are the three references every capability has access to.
22
+ * spread into the handler ctx for one dispatch. `envelope`, `container`,
23
+ * `runtime`, and the per-dispatch `signal` are the references every capability
24
+ * has access to. Thread `signal` into any nested dispatch a contributed verb
25
+ * starts, so caller cancellation propagates.
24
26
  */
25
27
  export interface CapabilityBase {
26
28
  readonly envelope: MessageEnvelope;
27
29
  readonly container: Container;
28
30
  readonly runtime: Runtime;
31
+ readonly signal: AbortSignal;
29
32
  }
30
33
  /**
31
34
  * A Capability contributes to the Runtime by reference.
@@ -38,6 +41,18 @@ export interface CapabilityBase {
38
41
  export interface Capability<TMark = unknown> {
39
42
  /** Stable identifier — used for diagnostics and duplicate-install detection. */
40
43
  readonly name: string;
44
+ /**
45
+ * Handler kinds this capability's ctx applies to. When set, `provideCtx`
46
+ * runs only for dispatches whose handler is one of these kinds — so an
47
+ * action's ctx carries `request`/`send`/`emit` while a query's does not.
48
+ * Omit for a universal contribution (the base every handler gets:
49
+ * `resolve`/`logger`/ambient). The dividing line is what the verb closes
50
+ * over: envelope/container-scoped verbs are kind-tagged here; instance-bound
51
+ * verbs (`assign`/`recordThat`) are layered on by the actor/workflow builder,
52
+ * not the runtime ctx. Values are `HandlerKind` strings ("action" / "query" /
53
+ * "handler" / …).
54
+ */
55
+ readonly kinds?: readonly string[];
41
56
  /**
42
57
  * Build the per-request ctx contribution. Called once per dispatch with
43
58
  * the current envelope + container scope. The returned object is spread
@@ -1,17 +1,43 @@
1
1
  /**
2
- * Per-runtime framework-hook registry. Each slot is a `Hook<TPayload>`
3
- * accessed as a typed property on `runtime.hooks`. Plugins extend via
4
- * TS module augmentation + `runtime.defineHook(name)`.
2
+ * Per-runtime delivery / plugin-extension hook registry. Each slot is a
3
+ * `Hook<TPayload>` accessed as a typed property on `runtime.hooks`. The
4
+ * runtime ships exactly one built-in slot — `LocalDelivery`, the ordered
5
+ * local fan-out chain. Plugins extend the registry via TS module
6
+ * augmentation + `runtime.defineHook(name)` (forge adds its Action* / Event*
7
+ * slots this way).
8
+ *
9
+ * App / plugin LIFECYCLE hooks are an App concern and live in `@nwire/app`'s
10
+ * `AppHooks` registry — not here. The runtime does not know about plugins.
5
11
  */
6
12
  import { type Hook } from "@nwire/hooks";
13
+ import type { MessageEnvelope } from "@nwire/envelope";
7
14
  /**
8
15
  * Plugins can distinguish "module compiled onto plugin lifecycle" from a
9
16
  * normal plugin. Lifecycle payloads include this so observers can filter.
10
17
  */
11
18
  export type PluginKind = "plugin" | "module";
12
19
  /**
13
- * The typed registry. Plugin packages augment this interface to add their
14
- * own slots; the foundation contributes the lifecycle slots below.
20
+ * Payload threaded through the `LocalDelivery` chain the one
21
+ * incoming-to-all delivery `runtime.emit` runs before fanning out to
22
+ * `when` listeners. Ordered fold steps (forge's idempotency, actors,
23
+ * projections, workflows) attach via `.use()` at their priority and
24
+ * read/mutate `deduped`: the idempotency gate sets it and vetoes (skips
25
+ * `next()`) to short-circuit both the rest of the chain AND the listener
26
+ * fan-out. Core-shaped on purpose — name + payload + envelope, no forge
27
+ * `EventMessage` type — so the runtime owns local delivery with zero forge
28
+ * dependency.
29
+ */
30
+ export interface LocalDeliveryPayload {
31
+ readonly eventName: string;
32
+ readonly payload: unknown;
33
+ readonly envelope: MessageEnvelope;
34
+ readonly dedupKey: string;
35
+ /** Set by an idempotency step when this event was seen before. */
36
+ deduped: boolean;
37
+ }
38
+ /**
39
+ * The typed delivery registry. Plugin packages augment this interface to add
40
+ * their own slots; the runtime contributes only the local-delivery slot.
15
41
  *
16
42
  * Every slot is `Hook<TPayload>` — no dispatch-mode discriminator.
17
43
  * Consumers call `.use()` to participate in the chain (with optional veto
@@ -19,85 +45,28 @@ export type PluginKind = "plugin" | "module";
19
45
  * receive step-level telemetry.
20
46
  */
21
47
  export interface FrameworkHooks {
22
- AppRegistering: Hook<{
23
- readonly appName: string;
24
- }>;
25
- AppBooting: Hook<{
26
- readonly appName: string;
27
- }>;
28
- AppBooted: Hook<{
29
- readonly appName: string;
30
- readonly bootedAt: string;
31
- }>;
32
- AppReady: Hook<{
33
- readonly appName: string;
34
- readonly readyAt: string;
35
- }>;
36
- AppShuttingDown: Hook<{
37
- readonly appName: string;
38
- readonly reason?: string;
39
- }>;
40
- AppShutdown: Hook<{
41
- readonly appName: string;
42
- }>;
43
- PluginRegistered: Hook<{
44
- readonly appName: string;
45
- readonly pluginName: string;
46
- readonly kind?: PluginKind;
47
- }>;
48
- PluginBooting: Hook<{
49
- readonly appName: string;
50
- readonly pluginName: string;
51
- readonly kind?: PluginKind;
52
- }>;
53
- PluginBooted: Hook<{
54
- readonly appName: string;
55
- readonly pluginName: string;
56
- readonly durationMs: number;
57
- readonly kind?: PluginKind;
58
- }>;
59
- PluginShuttingDown: Hook<{
60
- readonly appName: string;
61
- readonly pluginName: string;
62
- readonly kind?: PluginKind;
63
- }>;
64
- PluginShutdown: Hook<{
65
- readonly appName: string;
66
- readonly pluginName: string;
67
- readonly durationMs: number;
68
- readonly kind?: PluginKind;
69
- }>;
70
- WireMounting: Hook<{
71
- readonly appName: string;
72
- readonly transport: string;
73
- readonly manifest: unknown;
74
- }>;
75
- WireMounted: Hook<{
76
- readonly appName: string;
77
- readonly transport: string;
78
- readonly manifest: unknown;
79
- }>;
80
- WireUnmounted: Hook<{
81
- readonly appName: string;
82
- readonly transport: string;
83
- }>;
48
+ /**
49
+ * The one ordered local-delivery chain. `runtime.emit` runs it (veto via
50
+ * skipping `next()`), then fans out to listeners unless an upstream step
51
+ * deduped. Forge's fold steps (idempotency → actors → projections →
52
+ * workflows) attach here at priority; core ships it empty so a forge-less
53
+ * app's `emit` is just listener fan-out.
54
+ */
55
+ LocalDelivery: Hook<LocalDeliveryPayload>;
56
+ }
57
+ /**
58
+ * App / plugin LIFECYCLE hook registry — an App concern. The runtime does
59
+ * not own a lifecycle; `@nwire/app` augments this interface with the actual
60
+ * slots (App* / Plugin*) and owns the concrete registry instance (see
61
+ * `AppHooks` / `createAppHooks` in `@nwire/app`). It is declared here only
62
+ * so `PluginContext.on` / `PluginContext.defineHook` — which target app
63
+ * lifecycle hooks — stay typed without core-runtime depending on core-app.
64
+ */
65
+ export interface AppHooks {
84
66
  }
85
- /** Canonical names for the built-in slots — used to label the underlying Hooks. */
67
+ /** Canonical names for the built-in delivery slots — used to label the Hooks. */
86
68
  declare const BUILT_IN_HOOK_NAMES: {
87
- readonly AppRegistering: "nwire.app.registering";
88
- readonly AppBooting: "nwire.app.booting";
89
- readonly AppBooted: "nwire.app.booted";
90
- readonly AppReady: "nwire.app.ready";
91
- readonly AppShuttingDown: "nwire.app.shutting-down";
92
- readonly AppShutdown: "nwire.app.shutdown";
93
- readonly PluginRegistered: "nwire.plugin.registered";
94
- readonly PluginBooting: "nwire.plugin.booting";
95
- readonly PluginBooted: "nwire.plugin.booted";
96
- readonly PluginShuttingDown: "nwire.plugin.shutting-down";
97
- readonly PluginShutdown: "nwire.plugin.shutdown";
98
- readonly WireMounting: "nwire.wire.mounting";
99
- readonly WireMounted: "nwire.wire.mounted";
100
- readonly WireUnmounted: "nwire.wire.unmounted";
69
+ readonly LocalDelivery: "nwire.event.local-delivery";
101
70
  };
102
71
  /**
103
72
  * Construct the per-runtime registry, with every built-in slot
@@ -105,6 +74,6 @@ declare const BUILT_IN_HOOK_NAMES: {
105
74
  * land on the same registry instance.
106
75
  */
107
76
  export declare function createFrameworkHooks(): FrameworkHooks;
108
- /** True if `slot` is one of the built-in hook keys. */
77
+ /** True if `slot` is one of the built-in runtime hook keys. */
109
78
  export declare function isBuiltInHook(slot: string): slot is keyof typeof BUILT_IN_HOOK_NAMES;
110
79
  export {};
@@ -1,25 +1,18 @@
1
1
  /**
2
- * Per-runtime framework-hook registry. Each slot is a `Hook<TPayload>`
3
- * accessed as a typed property on `runtime.hooks`. Plugins extend via
4
- * TS module augmentation + `runtime.defineHook(name)`.
2
+ * Per-runtime delivery / plugin-extension hook registry. Each slot is a
3
+ * `Hook<TPayload>` accessed as a typed property on `runtime.hooks`. The
4
+ * runtime ships exactly one built-in slot — `LocalDelivery`, the ordered
5
+ * local fan-out chain. Plugins extend the registry via TS module
6
+ * augmentation + `runtime.defineHook(name)` (forge adds its Action* / Event*
7
+ * slots this way).
8
+ *
9
+ * App / plugin LIFECYCLE hooks are an App concern and live in `@nwire/app`'s
10
+ * `AppHooks` registry — not here. The runtime does not know about plugins.
5
11
  */
6
12
  import { hook } from "@nwire/hooks";
7
- /** Canonical names for the built-in slots — used to label the underlying Hooks. */
13
+ /** Canonical names for the built-in delivery slots — used to label the Hooks. */
8
14
  const BUILT_IN_HOOK_NAMES = {
9
- AppRegistering: "nwire.app.registering",
10
- AppBooting: "nwire.app.booting",
11
- AppBooted: "nwire.app.booted",
12
- AppReady: "nwire.app.ready",
13
- AppShuttingDown: "nwire.app.shutting-down",
14
- AppShutdown: "nwire.app.shutdown",
15
- PluginRegistered: "nwire.plugin.registered",
16
- PluginBooting: "nwire.plugin.booting",
17
- PluginBooted: "nwire.plugin.booted",
18
- PluginShuttingDown: "nwire.plugin.shutting-down",
19
- PluginShutdown: "nwire.plugin.shutdown",
20
- WireMounting: "nwire.wire.mounting",
21
- WireMounted: "nwire.wire.mounted",
22
- WireUnmounted: "nwire.wire.unmounted",
15
+ LocalDelivery: "nwire.event.local-delivery",
23
16
  };
24
17
  /**
25
18
  * Construct the per-runtime registry, with every built-in slot
@@ -33,7 +26,7 @@ export function createFrameworkHooks() {
33
26
  }
34
27
  return registry;
35
28
  }
36
- /** True if `slot` is one of the built-in hook keys. */
29
+ /** True if `slot` is one of the built-in runtime hook keys. */
37
30
  export function isBuiltInHook(slot) {
38
31
  return slot in BUILT_IN_HOOK_NAMES;
39
32
  }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * plus runtime alone is enough to dispatch handlers with envelopes, install
8
8
  * capabilities, install middleware, and react to events without any App.
9
9
  */
10
- export { Runtime, createRuntime, isBuiltInHook, serializeError, type RuntimeOptions, type Telemetry, type TelemetryListener, type HookStepTelemetry, type DispatchHookCtx, type DispatchMiddleware, type SerializedError, type PluginDefinition, type PluginContext, type ListenerContext, } from "./runtime.js";
11
- export { createFrameworkHooks, type FrameworkHooks, type PluginKind } from "./framework-hooks.js";
10
+ export { Runtime, createRuntime, isBuiltInHook, serializeError, messageRef, type MessageRef, type RuntimeOptions, type Telemetry, type TelemetryListener, type HookStepTelemetry, type SourceStageTelemetry, type SinkStageTelemetry, type DispatchHookCtx, type DispatchMiddleware, type SerializedError, type PluginDefinition, type PluginContext, type ListenerContext, } from "./runtime.js";
11
+ export { createFrameworkHooks, type FrameworkHooks, type AppHooks, type PluginKind, type LocalDeliveryPayload, } from "./framework-hooks.js";
12
12
  export { defineCapability, type Capability, type CapabilityBase } from "./capability.js";
13
13
  export type { OutboundStage, StagePosition, StageContext } from "./sink.js";
14
+ export type { InboundStage, InboundMessage, InboundContext, InboundTarget } from "./source.js";
15
+ export { installTelemetryReporter, type TelemetryReporter } from "./telemetry-sink.js";
package/dist/index.js CHANGED
@@ -8,7 +8,11 @@
8
8
  * capabilities, install middleware, and react to events without any App.
9
9
  */
10
10
  // Runtime core
11
- export { Runtime, createRuntime, isBuiltInHook, serializeError, } from "./runtime.js";
12
- export { createFrameworkHooks } from "./framework-hooks.js";
11
+ export { Runtime, createRuntime, isBuiltInHook, serializeError, messageRef, } from "./runtime.js";
12
+ export { createFrameworkHooks, } from "./framework-hooks.js";
13
13
  // Capability primitive — by-reference Runtime contribution.
14
14
  export { defineCapability } from "./capability.js";
15
+ // Telemetry sink — the terminal end of the onTelemetry source. Reporters are
16
+ // installed on the runtime; the concrete file/cloud drivers live in node
17
+ // packages that implement TelemetryReporter (fs-free here).
18
+ export { installTelemetryReporter } from "./telemetry-sink.js";
package/dist/runtime.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Runtime — Container, dispatch hook, FrameworkHooks registry, telemetry
3
- * stream, plugin lifecycle. The dispatch hook composes user middleware
2
+ * Runtime — Container, dispatch hook, delivery / plugin-extension hook
3
+ * registry, telemetry stream. The dispatch hook composes user middleware
4
4
  * via `runtime.use(...)` around an inner pinned step that calls the
5
- * registered handler. Plugins materialise additional FrameworkHooks
6
- * slots via `runtime.defineHook(name)` and TS module augmentation.
5
+ * registered handler. Plugins materialise additional `FrameworkHooks` slots
6
+ * via `runtime.defineHook(name)` and TS module augmentation. The runtime is
7
+ * a stateless substrate — it has no boot/shutdown lifecycle; the App owns
8
+ * that (see `AppHooks` / `AppLifecycle` in `@nwire/app`).
7
9
  */
8
10
  import { type Container } from "@nwire/container/awilix";
9
11
  import { type Hook } from "@nwire/hooks";
@@ -11,9 +13,10 @@ import { type Logger } from "@nwire/logger";
11
13
  import { type MessageEnvelope } from "@nwire/envelope";
12
14
  import type { HandlerDefinition } from "@nwire/handler";
13
15
  import type { EventDefinition, EventPayload } from "@nwire/messages";
14
- import { isBuiltInHook, type FrameworkHooks } from "./framework-hooks.js";
16
+ import { isBuiltInHook, type FrameworkHooks, type AppHooks } from "./framework-hooks.js";
15
17
  import type { Capability } from "./capability.js";
16
- import type { OutboundStage } from "./sink.js";
18
+ import type { OutboundStage, StagePosition } from "./sink.js";
19
+ import type { InboundStage, InboundMessage } from "./source.js";
17
20
  /**
18
21
  * Serialized form of any thrown value — `Error` instances keep `name` /
19
22
  * `message` / `stack` plus any own enumerable properties; non-Error throws
@@ -47,6 +50,43 @@ export interface HookStepTelemetry {
47
50
  readonly appName: string;
48
51
  readonly ts: string;
49
52
  }
53
+ /**
54
+ * `source.stage` — one record per installed inbound stage run (the terminal
55
+ * router is not recorded; its landing shows up as the `execute` / `emit` /
56
+ * fold records it triggers). `correlationId` ties the inbound pipeline to the
57
+ * dispatch it feeds, so Studio can draw one trace: inbound → execute → fold →
58
+ * outbound.
59
+ */
60
+ export interface SourceStageTelemetry {
61
+ readonly kind: "source.stage";
62
+ readonly stage: string;
63
+ readonly position: StagePosition;
64
+ readonly stageKind?: string;
65
+ readonly message?: {
66
+ readonly name?: string;
67
+ readonly kind?: "command" | "event";
68
+ };
69
+ readonly correlationId?: string;
70
+ readonly tenant?: string;
71
+ readonly shortCircuited: boolean;
72
+ readonly durationMs: number;
73
+ readonly appName: string;
74
+ readonly ts: string;
75
+ }
76
+ /** `sink.stage` — one record per outbound stage run during `sinkDrain`. */
77
+ export interface SinkStageTelemetry {
78
+ readonly kind: "sink.stage";
79
+ readonly stage: string;
80
+ readonly position: StagePosition;
81
+ readonly stageKind?: string;
82
+ readonly event: string;
83
+ readonly correlationId?: string;
84
+ readonly tenant?: string;
85
+ readonly shortCircuited: boolean;
86
+ readonly durationMs: number;
87
+ readonly appName: string;
88
+ readonly ts: string;
89
+ }
50
90
  /**
51
91
  * Base `Telemetry` union — generic kinds only. Subclasses (forge) WIDEN
52
92
  * this with their own domain kinds and re-export the wider union as their
@@ -54,7 +94,7 @@ export interface HookStepTelemetry {
54
94
  * everything; consumers that care about discriminants narrow with
55
95
  * `switch (rec.kind)`.
56
96
  */
57
- export type Telemetry = HookStepTelemetry;
97
+ export type Telemetry = HookStepTelemetry | SourceStageTelemetry | SinkStageTelemetry;
58
98
  export type TelemetryListener<T = Telemetry> = (record: T) => void;
59
99
  /**
60
100
  * Per-dispatch context passed through the `runtime.dispatch` hook. Subclass
@@ -74,6 +114,33 @@ export interface DispatchHookCtx {
74
114
  readonly coreFn: () => Promise<unknown>;
75
115
  result?: unknown;
76
116
  }
117
+ /**
118
+ * A handle to a dispatched message. Returned by the async dispatch verbs
119
+ * (`enqueue`, and forge's `send`) so the caller can correlate and observe a
120
+ * dispatch without blocking on its result. Every identifier comes from the
121
+ * envelope minted at dispatch time, so the ref matches the envelope the
122
+ * handler sees and every telemetry record that dispatch emits.
123
+ *
124
+ * The default is "ref, not result." Helpers that recover a result later
125
+ * (`awaitUntil`, `poll`) wrap a ref; they live outside the runtime so the
126
+ * hot dispatch path stays allocation-light.
127
+ */
128
+ export interface MessageRef {
129
+ /** The dispatched message's id — this dispatch's `envelope.messageId`. */
130
+ readonly messageId: string;
131
+ /** Correlation id shared across the whole causal chain. */
132
+ readonly correlationId: string;
133
+ /** What caused this dispatch — the parent's messageId (self at chain head). */
134
+ readonly causationId: string;
135
+ /** Logical target name — the handler / action / event name. */
136
+ readonly name: string;
137
+ /** Which door it went through. */
138
+ readonly kind: "command" | "event";
139
+ /** Tenant the dispatch is scoped to, when set. */
140
+ readonly tenant?: string;
141
+ }
142
+ /** Build a {@link MessageRef} from a minted envelope + target metadata. */
143
+ export declare function messageRef(envelope: MessageEnvelope, name: string, kind: "command" | "event"): MessageRef;
77
144
  /**
78
145
  * Onion-style extension point around action dispatch. Outermost first;
79
146
  * registration order is execution order (matches Koa/express semantics).
@@ -110,9 +177,12 @@ export declare class Runtime {
110
177
  protected readonly dispatchHook: Hook<DispatchHookCtx>;
111
178
  private userMiddlewareCount;
112
179
  /**
113
- * Per-runtime framework-hook registry. Built-in slots (App*, Plugin*,
114
- * Wire*) are pre-instantiated; plugins augment the `FrameworkHooks`
115
- * interface and materialise their slots via `defineHook(name)`.
180
+ * Per-runtime delivery / plugin-extension hook registry. The one built-in
181
+ * slot (`LocalDelivery`) is pre-instantiated; plugins augment the
182
+ * `FrameworkHooks` interface and materialise their slots via
183
+ * `defineHook(name)` (forge adds its Action* / Event* slots this way).
184
+ * App / plugin lifecycle hooks are an App concern — see `AppHooks` in
185
+ * `@nwire/app`.
116
186
  */
117
187
  readonly hooks: FrameworkHooks;
118
188
  private readonly telemetryListeners;
@@ -125,7 +195,7 @@ export declare class Runtime {
125
195
  private readonly handlers;
126
196
  /**
127
197
  * Per-event subscriber registry — keyed by `event.name`. Subscribers
128
- * attach via `runtime.subscribe(event, fn)` and fire on `runtime.emit(
198
+ * attach via `runtime.when(event, fn)` and fire on `runtime.emit(
129
199
  * event, payload)`. Foundation calls this out as the broadcast verb
130
200
  * distinct from telemetry-push (`runtime.pushTelemetry(record)`).
131
201
  */
@@ -150,6 +220,16 @@ export declare class Runtime {
150
220
  */
151
221
  private readonly sinkStages;
152
222
  private readonly terminalKinds;
223
+ /**
224
+ * Inbound source chain — populated by `runtime.source(stage)`, the mirror
225
+ * of the sink chain. `receive` runs them in position order (early → middle →
226
+ * terminal) and then the default `inboundRouter` lands the message on the
227
+ * one execution terminal. Terminal stages with a `kind` are exclusivity-
228
+ * checked, in a namespace independent of sink kinds.
229
+ */
230
+ private readonly sourceStages;
231
+ private readonly terminalSourceKinds;
232
+ private readonly inboundRouter;
153
233
  constructor(options?: RuntimeOptions);
154
234
  /**
155
235
  * Materialise a slot on `runtime.hooks` for a plugin-defined framework
@@ -190,12 +270,24 @@ export declare class Runtime {
190
270
  add(cap: Capability<unknown>): void;
191
271
  /** All installed capability names, in install order. Used by tests + Studio. */
192
272
  listCapabilities(): readonly string[];
273
+ /**
274
+ * Describe installed capabilities for the manifest/topology — name, the
275
+ * handler `kinds` it scopes to (undefined = universal), and the ctx keys it
276
+ * contributes (discovered by invoking `provideCtx` with a probe envelope).
277
+ * Read-only introspection; a `provideCtx` that throws on a bare probe yields
278
+ * `ctxKeys: []` rather than failing the scan.
279
+ */
280
+ describeCapabilities(): readonly {
281
+ readonly name: string;
282
+ readonly kinds?: readonly string[];
283
+ readonly ctxKeys: readonly string[];
284
+ }[];
193
285
  /**
194
286
  * Internal — called by `execute` to build the ctx contribution from every
195
287
  * installed capability for one dispatch. Public so subclass dispatchers
196
288
  * (forge) can compose; not part of the documented contract.
197
289
  */
198
- buildCapabilityCtx(envelope: MessageEnvelope): Record<string, unknown>;
290
+ buildCapabilityCtx(envelope: MessageEnvelope, signal: AbortSignal, kind?: string): Record<string, unknown>;
199
291
  /**
200
292
  * Install an outbound pipeline stage. Position-ordered: early → middle →
201
293
  * terminal. Within a position, install order is run order. Terminal stages
@@ -220,6 +312,48 @@ export declare class Runtime {
220
312
  }, payload: unknown, envelope: MessageEnvelope): Promise<void>;
221
313
  /** Stable position ordering: early → middle → terminal, install-order within. */
222
314
  private orderedSinkStages;
315
+ /**
316
+ * Install an inbound source stage — the mirror of `sink(stage)`. Position-
317
+ * ordered: early → middle → terminal; install order within a position.
318
+ * Terminal stages carrying a `kind` are deduplicated (one per kind), in a
319
+ * namespace independent of sink kinds.
320
+ */
321
+ source(stage: InboundStage): void;
322
+ /** All installed source stages plus the terminal router. Used by tests + Studio. */
323
+ listSourceStages(): readonly InboundStage[];
324
+ /**
325
+ * Inbound entry point — the mirror of `sinkDrain`. A transport (HTTP, queue,
326
+ * broker) hands a raw message here; every source stage runs in position
327
+ * order (early → middle → terminal), then the default router lands it on the
328
+ * one execution terminal. A stage returning `{ continue: false }` short-
329
+ * circuits the rest (e.g. a dedup stage dropping a replay).
330
+ *
331
+ * `opts.envelope` carries the seed / overrides the transport resolved
332
+ * (tenant, user, correlation); `opts.extras` threads transport context
333
+ * (logger, koa) onto the handler ctx. Returns the router's `ctx.result` — a
334
+ * command's handler result; events return nothing.
335
+ */
336
+ receive(message: InboundMessage, opts?: {
337
+ envelope?: Partial<MessageEnvelope>;
338
+ extras?: Record<string, unknown>;
339
+ /** Cancellation signal threaded onto the dispatch (queue lock, request abort). */
340
+ signal?: AbortSignal;
341
+ /**
342
+ * Parent envelope to inherit from — the message derives a child (carries
343
+ * correlationId, causationId = parent.messageId). Used by async sources
344
+ * (queue) that replay a stored envelope.
345
+ */
346
+ parent?: MessageEnvelope;
347
+ }): Promise<unknown>;
348
+ /** Stable inbound ordering: early → middle → terminal, router lands last. */
349
+ private orderedSourceStages;
350
+ /**
351
+ * The default terminal source stage — the router. A `command` lands on
352
+ * `execute` (by direct `target` reference, or by `name` through the
353
+ * registry); an `event` lands on `emit`. This is the one place inbound
354
+ * messages reach the execution terminal — no second dispatch path.
355
+ */
356
+ private buildInboundRouter;
223
357
  /**
224
358
  * Wire a hook's per-step tap into the canonical telemetry stream. After
225
359
  * this call, every `.use()` / `.on()` step on the hook emits a
@@ -242,8 +376,8 @@ export declare class Runtime {
242
376
  * subclass-widened records (CQRS kinds in forge) don't need cast
243
377
  * gymnastics — listeners narrow with `switch (rec.kind)`.
244
378
  *
245
- * Public so external dispatchers (forge's ForgeDispatcher composed
246
- * around a Runtime instance) can push records without subclassing.
379
+ * Public so forge's plugins (which ride this runtime rather than
380
+ * subclassing it) can push their own domain records.
247
381
  * Misuse risk is low: callers who own a Runtime are by definition
248
382
  * inside the trust boundary.
249
383
  *
@@ -264,6 +398,14 @@ export declare class Runtime {
264
398
  getHandler(name: string): HandlerDefinition<any, any, any>;
265
399
  /** All registered handler names — used by adapters to build wire tables. */
266
400
  listHandlers(): readonly string[];
401
+ /**
402
+ * Mint the delivery envelope for a dispatch. With a `parent` it derives a
403
+ * child (correlationId carried, causationId = parent.messageId); otherwise
404
+ * it seeds a fresh chain from the supplied overrides. The single place the
405
+ * derive-vs-seed choice lives — `execute`, `emit`, and `enqueue` share it
406
+ * so a ref minted up-front matches the envelope the handler sees.
407
+ */
408
+ private mintEnvelope;
267
409
  /**
268
410
  * Canonical sync dispatch verb. Validates input via the handler's input
269
411
  * schema, mints a child envelope (or seeds one when no parent is given),
@@ -277,21 +419,30 @@ export declare class Runtime {
277
419
  execute<TInput = unknown, TOutput = unknown>(handler: HandlerDefinition<any, TOutput, any> | string | ((input: TInput, ctx: any) => TOutput | Promise<TOutput>), input: TInput, envelopePartial?: Partial<MessageEnvelope> & {
278
420
  readonly signal?: AbortSignal;
279
421
  readonly parent?: MessageEnvelope;
422
+ /**
423
+ * A fully-minted envelope to use verbatim — skips derive/seed. Set by
424
+ * `enqueue`, which mints up-front so the `MessageRef` it returns names
425
+ * the same message the deferred dispatch runs under.
426
+ */
427
+ readonly envelope?: MessageEnvelope;
280
428
  }, extras?: Record<string, unknown>): Promise<TOutput>;
281
429
  /** Innermost dispatch step that calls the actual handler. Pinned once. */
282
430
  private dispatchCorePinned;
283
431
  private ensureDispatchCorePin;
284
432
  /**
285
- * Fire-and-forget dispatch. Default implementation: `setImmediate`
286
- * `execute`. Queue-adapter installation can override this to enqueue
287
- * onto an external queue (BullMQ, SQS, etc.). Errors are pushed onto
288
- * the telemetry stream as `kind: "enqueue.failed"` the caller has
289
- * already returned.
433
+ * Fire-and-forget dispatch. Mints the delivery envelope up-front, hands
434
+ * the deferred dispatch that exact envelope, and returns a {@link MessageRef}
435
+ * naming it so the caller can correlate / observe without blocking on the
436
+ * result. Default implementation: `setImmediate` `execute`. Queue-adapter
437
+ * installation can override this to enqueue onto an external queue (BullMQ,
438
+ * SQS, etc.) while keeping the same up-front envelope + ref contract. Errors
439
+ * surface on the telemetry stream as `kind: "enqueue.failed"` — the caller
440
+ * has already returned.
290
441
  */
291
442
  enqueue<TInput = unknown>(handler: HandlerDefinition<any, any, any> | string, input: TInput, envelopePartial?: Partial<MessageEnvelope> & {
292
443
  readonly signal?: AbortSignal;
293
444
  readonly parent?: MessageEnvelope;
294
- }): Promise<void>;
445
+ }): Promise<MessageRef>;
295
446
  /** Resolve a dependency from the runtime's container. */
296
447
  resolve<T = unknown>(name: string): T;
297
448
  /**
@@ -303,13 +454,6 @@ export declare class Runtime {
303
454
  when<E extends EventDefinition & {
304
455
  readonly name: string;
305
456
  }>(event: E, listener: (payload: EventPayload<E>, ctx: ListenerContext) => void | Promise<void>): () => void;
306
- /**
307
- * Alias for {@link when} — same behavior, same return. Preserved for
308
- * call sites that prefer the longer name.
309
- */
310
- subscribe<E extends EventDefinition & {
311
- readonly name: string;
312
- }>(event: E, listener: (payload: EventPayload<E>, ctx: ListenerContext) => void | Promise<void>): () => void;
313
457
  /**
314
458
  * Broadcast an event to its subscribers. Validates payload against the
315
459
  * event's schema, mints a child envelope, fires every registered
@@ -317,6 +461,19 @@ export declare class Runtime {
317
461
  * "event.emitted"` telemetry record. Subscriber throws are captured
318
462
  * but do not propagate to the caller.
319
463
  */
464
+ /**
465
+ * The one incoming-to-all LOCAL delivery. Runs the ordered `LocalDelivery`
466
+ * chain (forge's idempotency → actors → projections → workflows attach
467
+ * here); if an upstream step vetoed (`deduped`), stops — otherwise fans
468
+ * out to `when` listeners. Both `emit` (the validated entry)
469
+ * and forge's publish ride this, so there is one local-delivery path and
470
+ * no second fan-out. `envelope` is the already-minted delivery envelope —
471
+ * callers derive/seed it; `deliver` does not. Returns whether the event
472
+ * was deduped so a caller can gate its outbound `publish`.
473
+ */
474
+ deliver(eventName: string, payload: unknown, envelope: MessageEnvelope, dedupKey: string): Promise<{
475
+ deduped: boolean;
476
+ }>;
320
477
  emit<TPayload>(event: EventDefinition & {
321
478
  readonly name: string;
322
479
  readonly schema: {
@@ -395,15 +552,18 @@ export interface PluginDefinition<TOptions = void, TMark = unknown> {
395
552
  * What register/setup closures receive. One context type is used by both
396
553
  * phases. Members:
397
554
  *
398
- * - `container` / `runtime` / `hooks` — the layer references
555
+ * - `container` / `runtime` / `hooks` — the layer references. `hooks` is
556
+ * the runtime's delivery / plugin-extension registry; runtime concerns
557
+ * (`add` / `sink`) are reached through `runtime`.
399
558
  * - `options` — the typed value the plugin was constructed with
400
559
  * - `bind` — write a binding into the container
401
- * - `add(cap)` install a capability (proxies to `runtime.add`)
402
- * - `sink(stage)` — install an outbound pipeline stage
403
- * - `use(mw)` — wrap dispatch in middleware (via runtime)
404
- * - `on(hook, fn)` — observe a framework hook
560
+ * - `on(hook, fn)` observe an app lifecycle hook
405
561
  * - `boot(fn)` / `shutdown(fn)` — async lifecycle queues
406
- * - `defineHook(name)` — materialise a plugin-defined framework hook slot
562
+ * - `defineHook(name)` — materialise a plugin-defined app lifecycle slot
563
+ *
564
+ * Runtime concerns (`runtime.add(cap)`, `runtime.sink(stage)`,
565
+ * `runtime.use(mw)`) live on the `runtime` reference — the context exposes
566
+ * app concerns only.
407
567
  */
408
568
  export interface PluginContext<TOptions = void> {
409
569
  readonly container: Container;
@@ -415,21 +575,17 @@ export interface PluginContext<TOptions = void> {
415
575
  dispose?: (v: T) => void | Promise<void>;
416
576
  check?: (v: T) => void | Promise<void>;
417
577
  }): void;
418
- /** Install a capability — same as `runtime.add(cap)` but routed through this plugin. */
419
- add(cap: Capability<unknown>): void;
420
- /** Install an outbound pipeline stage. */
421
- sink(stage: OutboundStage): void;
422
578
  /**
423
- * Observe a framework lifecycle hook — calls `runtime.hooks[name].on(fn)`
424
- * under the hood. The payload is typed from the named hook, so
579
+ * Observe an app lifecycle hook — calls `appHooks[name].on(fn)` under the
580
+ * hood. The payload is typed from the named hook, so
425
581
  * `on("AppBooted", ({ appName, bootedAt }) => …)` is fully inferred.
426
582
  */
427
- on<K extends keyof FrameworkHooks>(name: K, fn: (payload: FrameworkHooks[K] extends Hook<infer P> ? P : never) => void): void;
583
+ on<K extends keyof AppHooks>(name: K, fn: (payload: AppHooks[K] extends Hook<infer P> ? P : never) => void): void;
428
584
  boot(fn: () => Promise<void> | void): void;
429
585
  /** Async cleanup work to run at `runtime.stop()`. */
430
586
  dispose(fn: () => Promise<void> | void): void;
431
587
  /** Alias for {@link dispose}. */
432
588
  shutdown(fn: () => Promise<void> | void): void;
433
- defineHook<TCtx>(name: keyof FrameworkHooks): Hook<TCtx>;
589
+ defineHook<TCtx>(name: keyof AppHooks): Hook<TCtx>;
434
590
  }
435
591
  export { isBuiltInHook };