@nwire/app 0.9.2 → 0.10.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.
Files changed (46) hide show
  1. package/README.md +81 -95
  2. package/dist/app.d.ts +7 -24
  3. package/dist/app.js +6 -24
  4. package/dist/compose-app.d.ts +35 -0
  5. package/dist/compose-app.js +138 -0
  6. package/dist/create-app.d.ts +38 -66
  7. package/dist/create-app.js +42 -216
  8. package/dist/define-plugin.d.ts +10 -149
  9. package/dist/define-plugin.js +11 -62
  10. package/dist/runtime/framework-hooks.d.ts +110 -0
  11. package/dist/runtime/framework-hooks.js +39 -0
  12. package/dist/runtime/index.d.ts +6 -0
  13. package/dist/runtime/index.js +6 -0
  14. package/dist/runtime/runtime.d.ts +349 -0
  15. package/dist/runtime/runtime.js +642 -0
  16. package/package.json +8 -5
  17. package/dist/__tests__/create-app.test.d.ts +0 -6
  18. package/dist/__tests__/create-app.test.d.ts.map +0 -1
  19. package/dist/__tests__/create-app.test.js +0 -126
  20. package/dist/__tests__/create-app.test.js.map +0 -1
  21. package/dist/__tests__/define-plugin.test.d.ts +0 -16
  22. package/dist/__tests__/define-plugin.test.d.ts.map +0 -1
  23. package/dist/__tests__/define-plugin.test.js +0 -269
  24. package/dist/__tests__/define-plugin.test.js.map +0 -1
  25. package/dist/__tests__/framework-events.test.d.ts +0 -18
  26. package/dist/__tests__/framework-events.test.d.ts.map +0 -1
  27. package/dist/__tests__/framework-events.test.js +0 -156
  28. package/dist/__tests__/framework-events.test.js.map +0 -1
  29. package/dist/app.d.ts.map +0 -1
  30. package/dist/app.js.map +0 -1
  31. package/dist/create-app.d.ts.map +0 -1
  32. package/dist/create-app.js.map +0 -1
  33. package/dist/define-plugin.d.ts.map +0 -1
  34. package/dist/define-plugin.js.map +0 -1
  35. package/dist/framework-event-bus.d.ts +0 -129
  36. package/dist/framework-event-bus.d.ts.map +0 -1
  37. package/dist/framework-event-bus.js +0 -188
  38. package/dist/framework-event-bus.js.map +0 -1
  39. package/dist/framework-events.d.ts +0 -233
  40. package/dist/framework-events.d.ts.map +0 -1
  41. package/dist/framework-events.js +0 -136
  42. package/dist/framework-events.js.map +0 -1
  43. package/dist/runtime.d.ts +0 -185
  44. package/dist/runtime.d.ts.map +0 -1
  45. package/dist/runtime.js +0 -197
  46. package/dist/runtime.js.map +0 -1
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runtime — Container + dispatch hook + per-runtime FrameworkHooks
3
+ * registry + telemetry stream + plugin lifecycle.
4
+ */
5
+ export { Runtime, createRuntime, isBuiltInHook, serializeError, type RuntimeOptions, type Telemetry, type TelemetryListener, type HookStepTelemetry, type DispatchHookCtx, type DispatchMiddleware, type SerializedError, type PluginDefinition, type PluginContext, } from "./runtime.js";
6
+ export { createFrameworkHooks, type FrameworkHooks, type PluginKind, } from "./framework-hooks.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runtime — Container + dispatch hook + per-runtime FrameworkHooks
3
+ * registry + telemetry stream + plugin lifecycle.
4
+ */
5
+ export { Runtime, createRuntime, isBuiltInHook, serializeError, } from "./runtime.js";
6
+ export { createFrameworkHooks, } from "./framework-hooks.js";
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Runtime — Container, dispatch hook, FrameworkHooks registry, telemetry
3
+ * stream, plugin lifecycle. The dispatch hook composes user middleware
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.
7
+ */
8
+ import { type Container } from "@nwire/container/awilix";
9
+ import { type Hook } from "@nwire/hooks";
10
+ import { type Logger } from "@nwire/logger";
11
+ import { type MessageEnvelope } from "@nwire/envelope";
12
+ import type { HandlerDefinition } from "@nwire/handler";
13
+ import type { EventDefinition } from "@nwire/messages";
14
+ import { isBuiltInHook, type FrameworkHooks } from "./framework-hooks.js";
15
+ /**
16
+ * Serialized form of any thrown value — `Error` instances keep `name` /
17
+ * `message` / `stack` plus any own enumerable properties; non-Error throws
18
+ * round-trip as `{ name: "NonError", message: String(err) }`. Used by every
19
+ * telemetry record that carries an error payload.
20
+ */
21
+ export type SerializedError = {
22
+ readonly name: string;
23
+ readonly message: string;
24
+ readonly stack?: string;
25
+ readonly [k: string]: unknown;
26
+ };
27
+ export declare function serializeError(err: unknown): SerializedError;
28
+ /**
29
+ * `hook.step` — one record per chain/listener phase on any adopted hook.
30
+ * `runId` / `parentRunId` link nested hook invocations into a causal tree —
31
+ * that's what Studio's Trace page uses.
32
+ */
33
+ export interface HookStepTelemetry {
34
+ readonly kind: "hook.step";
35
+ readonly hookName: string;
36
+ readonly hookId: string;
37
+ readonly runId: string;
38
+ readonly parentRunId?: string;
39
+ readonly stepId: number;
40
+ readonly stepKind: "chain" | "listener";
41
+ readonly stepName?: string;
42
+ readonly phase: "start" | "end" | "error";
43
+ readonly durationMs?: number;
44
+ readonly error?: SerializedError;
45
+ readonly appName: string;
46
+ readonly ts: string;
47
+ }
48
+ /**
49
+ * Base `Telemetry` union — generic kinds only. Subclasses (forge) WIDEN
50
+ * this with their own domain kinds and re-export the wider union as their
51
+ * own `Telemetry` alias. Consumers that listen on the base accept
52
+ * everything; consumers that care about discriminants narrow with
53
+ * `switch (rec.kind)`.
54
+ */
55
+ export type Telemetry = HookStepTelemetry;
56
+ export type TelemetryListener<T = Telemetry> = (record: T) => void;
57
+ /**
58
+ * Per-dispatch context passed through the `runtime.dispatch` hook. Subclass
59
+ * dispatchers populate `coreFn` with their retry-loop + handler-invocation
60
+ * closure; the pinned innermost chain step calls it and writes `result`.
61
+ * User middlewares attached via `runtime.use()` see this ctx via adapters
62
+ * that re-expose the legacy `(next, action, input, ctx)` shape.
63
+ *
64
+ * Types here are deliberately structural / unknown — the base runtime
65
+ * doesn't know what an "action" or "context" looks like; forge tightens
66
+ * the generics in its own re-export.
67
+ */
68
+ export interface DispatchHookCtx {
69
+ readonly action: unknown;
70
+ readonly input: unknown;
71
+ readonly ctx: unknown;
72
+ readonly coreFn: () => Promise<unknown>;
73
+ result?: unknown;
74
+ }
75
+ /**
76
+ * Onion-style extension point around action dispatch. Outermost first;
77
+ * registration order is execution order (matches Koa/express semantics).
78
+ * Middlewares run OUTSIDE any retry loop — one pass per dispatch.
79
+ */
80
+ export type DispatchMiddleware = (next: () => Promise<unknown>, action: unknown, input: unknown, ctx: unknown) => Promise<unknown>;
81
+ export interface RuntimeOptions {
82
+ readonly container?: Container;
83
+ readonly logger?: Logger;
84
+ /**
85
+ * App / service name. Stamped on every telemetry record so a single
86
+ * multi-tenant observer can demux records by origin.
87
+ */
88
+ readonly appName?: string;
89
+ }
90
+ export declare class Runtime {
91
+ protected readonly container: Container;
92
+ protected readonly _logger: Logger;
93
+ readonly appName: string;
94
+ /**
95
+ * Public accessor — external dispatchers compose runtimes and need the
96
+ * logger; test-kit harness extensions also swap it in for tap capture.
97
+ */
98
+ get logger(): Logger;
99
+ set logger(value: Logger);
100
+ /** Public accessor — external dispatchers register middleware via this hook. */
101
+ get dispatchHook$(): Hook<DispatchHookCtx>;
102
+ /**
103
+ * Dispatch substrate. Subclasses register their pinned `handler` step at
104
+ * priority `-Infinity` and call `dispatchHook.run(ctx)` from their own
105
+ * `dispatch` method. The tap forwards every chain step to the canonical
106
+ * telemetry stream.
107
+ */
108
+ protected readonly dispatchHook: Hook<DispatchHookCtx>;
109
+ private userMiddlewareCount;
110
+ /**
111
+ * Per-runtime framework-hook registry. Built-in slots (App*, Plugin*,
112
+ * Wire*) are pre-instantiated; plugins augment the `FrameworkHooks`
113
+ * interface and materialise their slots via `defineHook(name)`.
114
+ */
115
+ readonly hooks: FrameworkHooks;
116
+ private readonly telemetryListeners;
117
+ /**
118
+ * Handler registry — keyed by `handler.name`. Populated via
119
+ * `registerHandler(handler)` so consumers can dispatch by name:
120
+ * `runtime.execute("orders.place", input)`. Adapters (HTTP, queue, MCP)
121
+ * also iterate this for their own wire / tool tables.
122
+ */
123
+ private readonly handlers;
124
+ /**
125
+ * Per-event subscriber registry — keyed by `event.name`. Subscribers
126
+ * attach via `runtime.subscribe(event, fn)` and fire on `runtime.emit(
127
+ * event, payload)`. Foundation calls this out as the broadcast verb
128
+ * distinct from telemetry-push (`runtime.pushTelemetry(record)`).
129
+ */
130
+ private readonly eventListeners;
131
+ /**
132
+ * Registered plugins (one entry per `registerPlugin(def)` call). Each
133
+ * carries the captured boot/dispose closures the setup contributed.
134
+ * Boot fires in FIFO order at `start()`; dispose fires in LIFO order
135
+ * at `stop()`.
136
+ */
137
+ private readonly plugins;
138
+ /**
139
+ * Lifecycle state machine. Both `start()` and `stop()` are idempotent
140
+ * and in-flight-shared: a second call while the first is still pending
141
+ * returns the same Promise; a second call after completion is a no-op.
142
+ */
143
+ private lifecycle;
144
+ private startPromise;
145
+ private stopPromise;
146
+ constructor(options?: RuntimeOptions);
147
+ /**
148
+ * Materialise a slot on `runtime.hooks` for a plugin-defined framework
149
+ * hook. Idempotent — calling twice with the same name returns the same
150
+ * Hook. The slot is automatically observed for telemetry.
151
+ *
152
+ * Pair this with TS module augmentation:
153
+ *
154
+ * declare module "@nwire/app" {
155
+ * interface FrameworkHooks {
156
+ * MyEvent: Hook<{ tenant: string }>;
157
+ * }
158
+ * }
159
+ *
160
+ * const h = runtime.defineHook<{ tenant: string }>("MyEvent");
161
+ */
162
+ defineHook<TPayload>(name: keyof FrameworkHooks): Hook<TPayload>;
163
+ /**
164
+ * Register a dispatch middleware. Outermost first — the order you call
165
+ * `use()` is the order layers wrap (first `use` is the outermost layer).
166
+ * Middlewares run once per dispatch, outside the retry loop.
167
+ *
168
+ * Each user middleware gets a distinct negative priority so the chain
169
+ * order matches registration order; the pinned subclass-owned "handler"
170
+ * step at `-Infinity` stays strictly innermost.
171
+ */
172
+ use(middleware: DispatchMiddleware): void;
173
+ /**
174
+ * Wire a hook's per-step tap into the canonical telemetry stream. After
175
+ * this call, every `.use()` / `.on()` step on the hook emits a
176
+ * `kind: "hook.step"` record through `runtime.onTelemetry`, the same way
177
+ * the built-in `runtime.dispatch` hook does.
178
+ *
179
+ * The framework calls this for every framework hook in the registry;
180
+ * plugin authors can call it on their own hooks if they want their
181
+ * extension points surfaced in Studio / dev-logger / OTel for free.
182
+ */
183
+ observe<TCtx>(hk: Hook<TCtx>): void;
184
+ /**
185
+ * Subscribe to the canonical telemetry stream. Returns an unsubscribe.
186
+ * Throwing in a listener is caught and logged; never breaks dispatch.
187
+ */
188
+ onTelemetry<T = Telemetry>(listener: TelemetryListener<T>): () => void;
189
+ offTelemetry<T = Telemetry>(listener: TelemetryListener<T>): void;
190
+ /**
191
+ * Push a record onto the telemetry stream. Accepts `unknown` so
192
+ * subclass-widened records (CQRS kinds in forge) don't need cast
193
+ * gymnastics — listeners narrow with `switch (rec.kind)`.
194
+ *
195
+ * Public so external dispatchers (forge's ForgeDispatcher composed
196
+ * around a Runtime instance) can push records without subclassing.
197
+ * Misuse risk is low: callers who own a Runtime are by definition
198
+ * inside the trust boundary.
199
+ *
200
+ * Renamed from `emit` to free that name for the canonical event-
201
+ * broadcast verb (see `Runtime.emit(event, payload, envelope?)`).
202
+ */
203
+ pushTelemetry(record: unknown): void;
204
+ getContainer(): Container;
205
+ /** Current lifecycle phase. */
206
+ get state(): "idle" | "starting" | "running" | "stopping" | "stopped";
207
+ /**
208
+ * Register a plugin. The setup closure runs synchronously now —
209
+ * `bind()` calls land on the container immediately; `boot()` /
210
+ * `dispose()` calls accumulate queues that fire at `start()` /
211
+ * `stop()`. Returns the registered entry for callers that want it
212
+ * (rare — most consumers just discard).
213
+ *
214
+ * Plugin re-registration with the same `name` throws. Plugin setup
215
+ * may NOT await — async work belongs in `boot()`.
216
+ */
217
+ registerPlugin(plugin: PluginDefinition): RegisteredPlugin;
218
+ /**
219
+ * Two-phase boot. Idempotent + in-flight-shared.
220
+ *
221
+ * 1. Fire `PluginRegistered` for each registered plugin (observable).
222
+ * 2. Fire `AppRegistering` chain (vetoable via skip-next).
223
+ * 3. Fire `AppBooting` chain (vetoable via skip-next).
224
+ * 4. For each plugin in registration order:
225
+ * - fire `PluginBooting` chain (vetoable)
226
+ * - await every queued `boot()` fn for that plugin
227
+ * - fire `PluginBooted` (observable)
228
+ * 5. Run container health checks; fail-fast if any throw.
229
+ * 6. Fire `AppBooted` + `AppReady` (observable).
230
+ */
231
+ start(): Promise<void>;
232
+ /**
233
+ * Graceful shutdown. Idempotent + in-flight-shared.
234
+ *
235
+ * 1. Fire `AppShuttingDown` chain (vetoable via skip-next).
236
+ * 2. For each plugin in REVERSE registration order:
237
+ * - fire `PluginShuttingDown` (chain — refusal skips that
238
+ * plugin's dispose so downstream plugins still clean up)
239
+ * - run every queued `dispose()` fn in reverse, errors isolated
240
+ * - fire `PluginShutdown` (observable)
241
+ * 3. `container.dispose()` — runs every bind({ dispose }) in LIFO.
242
+ * 4. Fire `AppShutdown` (observable).
243
+ */
244
+ stop(reason?: string): Promise<void>;
245
+ /**
246
+ * Register a handler so it can be executed by string name. Stamps the
247
+ * handler into the runtime's registry and into the container under the
248
+ * key `handler:<name>` so middleware that needs to resolve it can.
249
+ */
250
+ registerHandler(handler: HandlerDefinition<any, any, any>): void;
251
+ /**
252
+ * Look up a handler by name; throws if not registered.
253
+ */
254
+ getHandler(name: string): HandlerDefinition<any, any, any>;
255
+ /** All registered handler names — used by adapters to build wire tables. */
256
+ listHandlers(): readonly string[];
257
+ /**
258
+ * Canonical sync dispatch verb. Validates input via the handler's input
259
+ * schema, mints a child envelope (or seeds one when no parent is given),
260
+ * builds a per-request scope on the container, threads the result
261
+ * through the handler's `.use()` chain (and `.on()` listeners), then
262
+ * disposes the scope. Returns the handler's `ctx.result`.
263
+ *
264
+ * `handler` may be a `HandlerDefinition` reference (preferred) or a
265
+ * string name registered via `registerHandler()`.
266
+ */
267
+ execute<TInput = unknown, TOutput = unknown>(handler: HandlerDefinition<any, TOutput, any> | string | ((input: TInput, ctx: any) => TOutput | Promise<TOutput>), input: TInput, envelopePartial?: Partial<MessageEnvelope> & {
268
+ readonly signal?: AbortSignal;
269
+ readonly parent?: MessageEnvelope;
270
+ }, extras?: Record<string, unknown>): Promise<TOutput>;
271
+ /**
272
+ * Fire-and-forget dispatch. Default implementation: `setImmediate` →
273
+ * `execute`. Queue-adapter installation can override this to enqueue
274
+ * onto an external queue (BullMQ, SQS, etc.). Errors are pushed onto
275
+ * the telemetry stream as `kind: "enqueue.failed"` — the caller has
276
+ * already returned.
277
+ */
278
+ enqueue<TInput = unknown>(handler: HandlerDefinition<any, any, any> | string, input: TInput, envelopePartial?: Partial<MessageEnvelope> & {
279
+ readonly signal?: AbortSignal;
280
+ readonly parent?: MessageEnvelope;
281
+ }): Promise<void>;
282
+ /**
283
+ * Subscribe to an event. Returns an unsubscribe. Listeners are stored
284
+ * per `event.name` and fire in registration order via `Promise.allSettled`
285
+ * when `emit(event, ...)` is called. Throwing listeners are captured in
286
+ * telemetry but do not break sibling listeners.
287
+ */
288
+ subscribe<TPayload>(event: EventDefinition & {
289
+ readonly name: string;
290
+ }, listener: (payload: TPayload, envelope: MessageEnvelope) => void | Promise<void>): () => void;
291
+ /**
292
+ * Broadcast an event to its subscribers. Validates payload against the
293
+ * event's schema, mints a child envelope, fires every registered
294
+ * subscriber in parallel (allSettled), and records a `kind:
295
+ * "event.emitted"` telemetry record. Subscriber throws are captured
296
+ * but do not propagate to the caller.
297
+ */
298
+ emit<TPayload>(event: EventDefinition & {
299
+ readonly name: string;
300
+ readonly schema: {
301
+ parse(input: unknown): unknown;
302
+ };
303
+ }, payload: TPayload, envelopePartial?: Partial<MessageEnvelope> & {
304
+ readonly parent?: MessageEnvelope;
305
+ }): Promise<void>;
306
+ }
307
+ /**
308
+ * Canonical factory for a Runtime. Matches the foundation-doc shape
309
+ * (`createRuntime(opts)`) so consumer code stays uniform across
310
+ * primitives — `createContainer()`, `createInterface()`, `createApp()`,
311
+ * `createRuntime()`. The `new Runtime(opts)` form remains available for
312
+ * subclasses + tests that need to override protected members.
313
+ */
314
+ export declare function createRuntime(options?: RuntimeOptions): Runtime;
315
+ /**
316
+ * Plugin definition. `setup` runs synchronously at `registerPlugin()`
317
+ * time — `bind()` calls land on the container immediately; `boot()` /
318
+ * `dispose()` calls accumulate queues that fire at `start()` / `stop()`.
319
+ *
320
+ * Async work belongs in `boot()`. Setup throws abort registration.
321
+ */
322
+ export interface PluginDefinition {
323
+ readonly name: string;
324
+ setup(ctx: PluginContext): void;
325
+ }
326
+ /**
327
+ * What the setup closure receives. `bind` writes to the container;
328
+ * `boot` / `dispose` queue async work; `hooks` + `defineHook` expose
329
+ * the per-runtime framework-hook registry.
330
+ */
331
+ export interface PluginContext {
332
+ readonly container: Container;
333
+ readonly runtime: Runtime;
334
+ readonly hooks: FrameworkHooks;
335
+ bind<T>(name: string, factory: T | (() => T) | (new (cradle: unknown) => T), opts?: {
336
+ dispose?: (v: T) => void | Promise<void>;
337
+ check?: (v: T) => void | Promise<void>;
338
+ }): void;
339
+ boot(fn: () => Promise<void> | void): void;
340
+ dispose(fn: () => Promise<void> | void): void;
341
+ defineHook<TCtx>(name: keyof FrameworkHooks): Hook<TCtx>;
342
+ }
343
+ /** Internal record kept on the runtime for each plugin. */
344
+ interface RegisteredPlugin {
345
+ readonly name: string;
346
+ readonly boots: Array<() => Promise<void> | void>;
347
+ readonly disposes: Array<() => Promise<void> | void>;
348
+ }
349
+ export { isBuiltInHook };