@nwire/app 0.7.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `definePlugin` — the narrow, generic plugin primitive for `@nwire/app`.
3
+ *
4
+ * Plugins are the ONLY extension primitive in the sealed architecture.
5
+ * They install once, run during app lifecycle, and contribute three things:
6
+ *
7
+ * 1. **Container bindings** via `provide(name, lifecycle)` — bootable
8
+ * values (DB pool, Redis client, HTTP client, anything that needs
9
+ * `boot()` + optional `shutdown()` + optional `healthCheck()`).
10
+ * 2. **Event subscriptions** via `on(FrameworkEvent, handler, priority?)`
11
+ * — react to app lifecycle, custom events, or anything else fired
12
+ * through the FrameworkEventBus.
13
+ * 3. **Lifecycle callbacks** via `boot(fn)` + `shutdown(fn)` — async
14
+ * work at known transitions (post-provider-boot, pre-shutdown).
15
+ *
16
+ * This narrow shape covers 100% of "extend the app" cases that don't
17
+ * need forge-specific knowledge. For plugins that need action middleware,
18
+ * actor lifecycle hooks, or `before("action.name", ...)` sugar, use the
19
+ * richer `definePlugin` in `@nwire/forge` — it's a superset that wraps
20
+ * this one.
21
+ *
22
+ * ## When to reach for this vs forge's
23
+ *
24
+ * - Connecting a database / cache / queue / SDK? → this one.
25
+ * - Tracing every dispatched action? → forge's (needs middleware).
26
+ * - Reacting to a workflow saga timer? → forge's (needs actor hooks).
27
+ * - Multi-tenant request context propagation? → forge's (needs middleware).
28
+ * - Custom framework events fired by your own code? → this one.
29
+ *
30
+ * In other words: if you don't need to peek inside action dispatch or
31
+ * actor transitions, you don't need forge's plugin form. Use this one.
32
+ */
33
+ import type { Container } from "@nwire/container";
34
+ import type { FrameworkEventDefinition } from "./framework-events.js";
35
+ import type { FrameworkEventBus, FrameworkEventHandler } from "./framework-event-bus.js";
36
+ /**
37
+ * Describes a value the plugin produces. The framework calls `boot()` to
38
+ * obtain it during app start, registers it on the container under the
39
+ * plugin's chosen name, calls `healthCheck` periodically (if provided),
40
+ * and calls `shutdown(value)` during graceful drain.
41
+ */
42
+ export interface BindingLifecycle<T> {
43
+ boot(): Promise<T> | T;
44
+ shutdown?(value: T): Promise<void> | void;
45
+ healthCheck?(value: T): Promise<void> | void;
46
+ }
47
+ /**
48
+ * What the setup closure gets. Every method is a builder — each call
49
+ * accrues into the plugin's contribution to the app's lifecycle. There's
50
+ * no separate "apply" step; the framework collects everything the closure
51
+ * does and threads it into the right lifecycle phases.
52
+ */
53
+ export interface AppPluginContext {
54
+ /**
55
+ * The app's container. Read-only here; the plugin contributes bindings
56
+ * via `provide()`, not by writing to the container directly. This
57
+ * reference is exposed for the rare case where the plugin needs to
58
+ * inspect existing bindings (e.g., feature detection).
59
+ */
60
+ readonly container: Container;
61
+ /**
62
+ * The app's framework-event bus. Exposed for advanced cases; most
63
+ * subscriptions go through `on()` for ergonomic registration.
64
+ */
65
+ readonly bus: FrameworkEventBus;
66
+ /**
67
+ * Register a bootable binding. The framework calls `lifecycle.boot()`
68
+ * during app start, stores the result on the container under `name`,
69
+ * and wires `shutdown` + `healthCheck` into the lifecycle.
70
+ *
71
+ * ```ts
72
+ * provide("db", {
73
+ * boot: () => new Pool(env.DATABASE_URL),
74
+ * shutdown: (pool) => pool.end(),
75
+ * healthCheck: (pool) => pool.query("SELECT 1"),
76
+ * })
77
+ * ```
78
+ */
79
+ provide<T>(name: string, lifecycle: BindingLifecycle<T>): void;
80
+ /**
81
+ * Subscribe to a framework event. Higher priorities run earlier in
82
+ * series/series-bail modes (default 0; framework built-ins should use
83
+ * 100+; debug/observer plugins -100).
84
+ */
85
+ on<TPayload>(event: FrameworkEventDefinition<TPayload>, handler: FrameworkEventHandler<TPayload>, priority?: number): void;
86
+ /**
87
+ * Register a callback that runs once during app boot, after all
88
+ * provided bindings are registered and other plugins' `boot` callbacks
89
+ * have run. Use for warm-up work: prefetching, idempotency caches,
90
+ * pre-flight connectivity checks beyond the basic healthCheck.
91
+ */
92
+ boot(fn: () => Promise<void> | void): void;
93
+ /**
94
+ * Register a callback that runs during graceful shutdown, BEFORE
95
+ * provided bindings' `shutdown(value)` callbacks. Use for "flush pending
96
+ * work" style cleanup — finalizing analytics events, draining queues
97
+ * the plugin owns directly, etc.
98
+ */
99
+ shutdown(fn: () => Promise<void> | void): void;
100
+ }
101
+ /**
102
+ * What the framework receives. Opaque to user code — it carries the setup
103
+ * function the app's lifecycle will invoke during boot, plus identity
104
+ * metadata for diagnostics.
105
+ *
106
+ * Note the `$nwireAppPlugin: true` marker — lets the runtime discriminate
107
+ * this from forge's richer `PluginDefinition` (which has its own marker).
108
+ * Both shapes can be used in the same app's plugin list; the runtime
109
+ * dispatches each through the right wiring path.
110
+ */
111
+ export interface AppPluginDefinition {
112
+ readonly $nwireAppPlugin: true;
113
+ readonly name: string;
114
+ /**
115
+ * The setup closure. The framework calls this during plugin
116
+ * registration with an `AppPluginContext` bound to the app's container
117
+ * + bus. Returns void or a Promise; if async, the framework awaits
118
+ * before proceeding to provider boot.
119
+ */
120
+ readonly setup: (ctx: AppPluginContext) => void | Promise<void>;
121
+ }
122
+ /**
123
+ * Build a plugin. The returned value is opaque — pass it to your app's
124
+ * plugin list. The framework invokes the setup closure once during boot
125
+ * with a context bound to the app's container + bus.
126
+ *
127
+ * ```ts
128
+ * import { definePlugin } from "@nwire/app"
129
+ * import { AppBooted } from "@nwire/app"
130
+ *
131
+ * export const tracingPlugin = definePlugin("tracing", ({ provide, on, boot }) => {
132
+ * provide("tracer", {
133
+ * boot: () => new OtelTracer({ exporter: process.env.OTEL_ENDPOINT }),
134
+ * shutdown: (t) => t.shutdown(),
135
+ * })
136
+ *
137
+ * on(AppBooted, ({ appName }) => {
138
+ * console.log(`tracing started for ${appName}`)
139
+ * })
140
+ *
141
+ * boot(async () => {
142
+ * // warm up — initialize span context, register process exit handlers, …
143
+ * })
144
+ * })
145
+ * ```
146
+ */
147
+ export declare function definePlugin(name: string, setup: (ctx: AppPluginContext) => void | Promise<void>): AppPluginDefinition;
148
+ /** Type guard — does this look like an app-layer plugin definition? */
149
+ export declare function isAppPlugin(x: unknown): x is AppPluginDefinition;
150
+ //# sourceMappingURL=define-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-plugin.d.ts","sourceRoot":"","sources":["../src/define-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAItF;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACvB,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,WAAW,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC9C;AAID;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC;IAEhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE/D;;;;OAIG;IACH,EAAE,CAAC,QAAQ,EACT,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EACzC,OAAO,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EACxC,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAAC;IAER;;;;;OAKG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAE3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;CAChD;AAID;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAID;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACrD,mBAAmB,CAMrB;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,mBAAmB,CAMhE"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `definePlugin` — the narrow, generic plugin primitive for `@nwire/app`.
3
+ *
4
+ * Plugins are the ONLY extension primitive in the sealed architecture.
5
+ * They install once, run during app lifecycle, and contribute three things:
6
+ *
7
+ * 1. **Container bindings** via `provide(name, lifecycle)` — bootable
8
+ * values (DB pool, Redis client, HTTP client, anything that needs
9
+ * `boot()` + optional `shutdown()` + optional `healthCheck()`).
10
+ * 2. **Event subscriptions** via `on(FrameworkEvent, handler, priority?)`
11
+ * — react to app lifecycle, custom events, or anything else fired
12
+ * through the FrameworkEventBus.
13
+ * 3. **Lifecycle callbacks** via `boot(fn)` + `shutdown(fn)` — async
14
+ * work at known transitions (post-provider-boot, pre-shutdown).
15
+ *
16
+ * This narrow shape covers 100% of "extend the app" cases that don't
17
+ * need forge-specific knowledge. For plugins that need action middleware,
18
+ * actor lifecycle hooks, or `before("action.name", ...)` sugar, use the
19
+ * richer `definePlugin` in `@nwire/forge` — it's a superset that wraps
20
+ * this one.
21
+ *
22
+ * ## When to reach for this vs forge's
23
+ *
24
+ * - Connecting a database / cache / queue / SDK? → this one.
25
+ * - Tracing every dispatched action? → forge's (needs middleware).
26
+ * - Reacting to a workflow saga timer? → forge's (needs actor hooks).
27
+ * - Multi-tenant request context propagation? → forge's (needs middleware).
28
+ * - Custom framework events fired by your own code? → this one.
29
+ *
30
+ * In other words: if you don't need to peek inside action dispatch or
31
+ * actor transitions, you don't need forge's plugin form. Use this one.
32
+ */
33
+ // ─── The factory ──────────────────────────────────────────────────
34
+ /**
35
+ * Build a plugin. The returned value is opaque — pass it to your app's
36
+ * plugin list. The framework invokes the setup closure once during boot
37
+ * with a context bound to the app's container + bus.
38
+ *
39
+ * ```ts
40
+ * import { definePlugin } from "@nwire/app"
41
+ * import { AppBooted } from "@nwire/app"
42
+ *
43
+ * export const tracingPlugin = definePlugin("tracing", ({ provide, on, boot }) => {
44
+ * provide("tracer", {
45
+ * boot: () => new OtelTracer({ exporter: process.env.OTEL_ENDPOINT }),
46
+ * shutdown: (t) => t.shutdown(),
47
+ * })
48
+ *
49
+ * on(AppBooted, ({ appName }) => {
50
+ * console.log(`tracing started for ${appName}`)
51
+ * })
52
+ *
53
+ * boot(async () => {
54
+ * // warm up — initialize span context, register process exit handlers, …
55
+ * })
56
+ * })
57
+ * ```
58
+ */
59
+ export function definePlugin(name, setup) {
60
+ return {
61
+ $nwireAppPlugin: true,
62
+ name,
63
+ setup,
64
+ };
65
+ }
66
+ /** Type guard — does this look like an app-layer plugin definition? */
67
+ export function isAppPlugin(x) {
68
+ return (typeof x === "object" &&
69
+ x !== null &&
70
+ x.$nwireAppPlugin === true);
71
+ }
72
+ //# sourceMappingURL=define-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-plugin.js","sourceRoot":"","sources":["../src/define-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA8GH,qEAAqE;AAErE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,KAAsD;IAEtD,OAAO;QACL,eAAe,EAAE,IAAI;QACrB,IAAI;QACJ,KAAK;KACN,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAC,CAAU;IACpC,OAAO,CACL,OAAO,CAAC,KAAK,QAAQ;QACrB,CAAC,KAAK,IAAI;QACT,CAAmC,CAAC,eAAe,KAAK,IAAI,CAC9D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `FrameworkEventBus` — the dispatcher behind framework events.
3
+ *
4
+ * Owned by an app's runtime. One bus per app instance. Plugins, apps,
5
+ * and modules subscribe with `bus.on(Event, handler)`; the runtime fires
6
+ * events at the appropriate lifecycle points with `bus.fire(Event, payload)`.
7
+ *
8
+ * Dispatch semantics come from the event's `mode`:
9
+ *
10
+ * parallel → all handlers concurrently; one failing doesn't affect
11
+ * others; errors are logged via the supplied logger.
12
+ * series → sequential await; one failing stops the chain and throws.
13
+ * series-bail → sequential await; a handler returning `false` stops the
14
+ * chain cleanly (no throw) and `fire` returns false to
15
+ * signal "prevented". Returning anything else (including
16
+ * undefined/void) is treated as "ok, continue".
17
+ *
18
+ * The series-bail mode is the interceptable hook — `*-ing` events use it
19
+ * so plugins can veto an action, refuse a shutdown, etc.
20
+ *
21
+ * The bus lives in `@nwire/app` (the "managed Container with lifecycle"
22
+ * layer). `@nwire/forge` re-exports the same surface so consumers that
23
+ * pull from forge get one import line.
24
+ */
25
+ import type { FrameworkEventDefinition } from "./framework-events.js";
26
+ import type { Logger } from "@nwire/logger";
27
+ /**
28
+ * Handler signature. The handler MAY:
29
+ * - return nothing → "continue"
30
+ * - return false → "prevent" (series-bail only)
31
+ * - return a Promise resolving to either
32
+ * - throw → "fail" (propagates in series modes,
33
+ * logged-and-swallowed in parallel)
34
+ *
35
+ * In parallel/series modes the return value is ignored entirely.
36
+ */
37
+ export type FrameworkEventHandler<TPayload> = (payload: TPayload) => Promise<void | boolean> | void | boolean;
38
+ /**
39
+ * Per-fire observer — receives every framework-event firing on the bus.
40
+ * Used by the dev logger, Studio Live stream, and OTel exporter to surface
41
+ * lifecycle activity without each one re-subscribing per event type.
42
+ *
43
+ * `phase: "fired"` — handler chain completed without a veto.
44
+ * `phase: "prevented"` — a series-bail handler returned false.
45
+ */
46
+ export interface FrameworkEventObservation {
47
+ readonly eventName: string;
48
+ readonly payload: unknown;
49
+ readonly mode: "parallel" | "series" | "series-bail";
50
+ readonly phase: "fired" | "prevented";
51
+ readonly ts: string;
52
+ }
53
+ export type FrameworkEventObserver = (record: FrameworkEventObservation) => void;
54
+ /**
55
+ * The bus. Subscriptions and dispatch live here so an app's runtime stays
56
+ * focused on the domain pipeline. Designed to be reusable — Studio, the
57
+ * orchestrator, and external plugins all hit the same surface.
58
+ */
59
+ export declare class FrameworkEventBus {
60
+ private readonly subs;
61
+ private readonly observers;
62
+ private readonly logger;
63
+ constructor(logger: Logger);
64
+ /**
65
+ * Subscribe to EVERY framework-event firing on this bus. Used by the
66
+ * dev logger, Studio Live, and OTel exporter to surface lifecycle
67
+ * activity without each one re-subscribing per event type.
68
+ *
69
+ * Returns an unsubscribe handle.
70
+ */
71
+ onFire(observer: FrameworkEventObserver): () => void;
72
+ private notify;
73
+ /**
74
+ * Subscribe to a framework event. `priority` controls ordering for
75
+ * series-bail / series modes (higher runs first; default 0). Framework
76
+ * built-ins should subscribe at priority 100+; debug/observer plugins
77
+ * at -100 so they see the final post-mutation state.
78
+ */
79
+ on<TPayload>(event: FrameworkEventDefinition<TPayload>, handler: FrameworkEventHandler<TPayload>, priority?: number): () => void;
80
+ /**
81
+ * Fire a framework event. Resolves to `false` when a series-bail handler
82
+ * vetoed; `true` otherwise. The boolean is irrelevant for parallel /
83
+ * series modes and always returns `true` there for them.
84
+ */
85
+ fire<TPayload>(event: FrameworkEventDefinition<TPayload>, payload: TPayload): Promise<boolean>;
86
+ /** Count subscribers for an event — diagnostic surface (Studio, tests). */
87
+ subscriberCount(event: FrameworkEventDefinition): number;
88
+ }
89
+ //# sourceMappingURL=framework-event-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework-event-bus.d.ts","sourceRoot":"","sources":["../src/framework-event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,CAAC,QAAQ,IAAI,CAC5C,OAAO,EAAE,QAAQ,KACd,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC;AAO9C;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AACD,MAAM,MAAM,sBAAsB,GAAG,CAAC,MAAM,EAAE,yBAAyB,KAAK,IAAI,CAAC;AAEjF;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8C;IACnE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,MAAM;IAI1B;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,IAAI;IAQpD,OAAO,CAAC,MAAM;IAgBd;;;;;OAKG;IACH,EAAE,CAAC,QAAQ,EACT,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EACzC,OAAO,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EACxC,QAAQ,GAAE,MAAU,GACnB,MAAM,IAAI;IAgBb;;;;OAIG;IACG,IAAI,CAAC,QAAQ,EACjB,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EACzC,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;IA8DnB,2EAA2E;IAC3E,eAAe,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM;CAGzD"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `FrameworkEventBus` — the dispatcher behind framework events.
3
+ *
4
+ * Owned by an app's runtime. One bus per app instance. Plugins, apps,
5
+ * and modules subscribe with `bus.on(Event, handler)`; the runtime fires
6
+ * events at the appropriate lifecycle points with `bus.fire(Event, payload)`.
7
+ *
8
+ * Dispatch semantics come from the event's `mode`:
9
+ *
10
+ * parallel → all handlers concurrently; one failing doesn't affect
11
+ * others; errors are logged via the supplied logger.
12
+ * series → sequential await; one failing stops the chain and throws.
13
+ * series-bail → sequential await; a handler returning `false` stops the
14
+ * chain cleanly (no throw) and `fire` returns false to
15
+ * signal "prevented". Returning anything else (including
16
+ * undefined/void) is treated as "ok, continue".
17
+ *
18
+ * The series-bail mode is the interceptable hook — `*-ing` events use it
19
+ * so plugins can veto an action, refuse a shutdown, etc.
20
+ *
21
+ * The bus lives in `@nwire/app` (the "managed Container with lifecycle"
22
+ * layer). `@nwire/forge` re-exports the same surface so consumers that
23
+ * pull from forge get one import line.
24
+ */
25
+ /**
26
+ * The bus. Subscriptions and dispatch live here so an app's runtime stays
27
+ * focused on the domain pipeline. Designed to be reusable — Studio, the
28
+ * orchestrator, and external plugins all hit the same surface.
29
+ */
30
+ export class FrameworkEventBus {
31
+ subs = new Map();
32
+ observers = [];
33
+ logger;
34
+ constructor(logger) {
35
+ this.logger = logger;
36
+ }
37
+ /**
38
+ * Subscribe to EVERY framework-event firing on this bus. Used by the
39
+ * dev logger, Studio Live, and OTel exporter to surface lifecycle
40
+ * activity without each one re-subscribing per event type.
41
+ *
42
+ * Returns an unsubscribe handle.
43
+ */
44
+ onFire(observer) {
45
+ this.observers.push(observer);
46
+ return () => {
47
+ const i = this.observers.indexOf(observer);
48
+ if (i >= 0)
49
+ this.observers.splice(i, 1);
50
+ };
51
+ }
52
+ notify(rec) {
53
+ // Observers run after the actual fire — they MUST NOT veto or alter
54
+ // semantics. Errors are swallowed + logged so a buggy observer can't
55
+ // break the lifecycle.
56
+ for (const o of this.observers) {
57
+ try {
58
+ o(rec);
59
+ }
60
+ catch (err) {
61
+ this.logger.error?.("framework-event observer threw", {
62
+ event: rec.eventName,
63
+ error: err?.message ?? String(err),
64
+ });
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Subscribe to a framework event. `priority` controls ordering for
70
+ * series-bail / series modes (higher runs first; default 0). Framework
71
+ * built-ins should subscribe at priority 100+; debug/observer plugins
72
+ * at -100 so they see the final post-mutation state.
73
+ */
74
+ on(event, handler, priority = 0) {
75
+ const list = this.subs.get(event.name) ?? [];
76
+ const sub = { handler, priority };
77
+ list.push(sub);
78
+ // Sort by priority desc so higher-priority handlers run first.
79
+ list.sort((a, b) => b.priority - a.priority);
80
+ this.subs.set(event.name, list);
81
+ return () => {
82
+ const current = this.subs.get(event.name);
83
+ if (!current)
84
+ return;
85
+ const i = current.indexOf(sub);
86
+ if (i >= 0)
87
+ current.splice(i, 1);
88
+ };
89
+ }
90
+ /**
91
+ * Fire a framework event. Resolves to `false` when a series-bail handler
92
+ * vetoed; `true` otherwise. The boolean is irrelevant for parallel /
93
+ * series modes and always returns `true` there for them.
94
+ */
95
+ async fire(event, payload) {
96
+ const handlers = (this.subs.get(event.name) ?? []);
97
+ // Always notify observers — even when no subscribers exist for this
98
+ // event. The lifecycle is the signal worth recording; subscribers are
99
+ // an orthogonal concern (could be none in dev, several in prod).
100
+ const ts = new Date().toISOString();
101
+ const obsRec = (phase) => ({
102
+ eventName: event.name,
103
+ payload,
104
+ mode: event.mode,
105
+ phase,
106
+ ts,
107
+ });
108
+ if (handlers.length === 0) {
109
+ this.notify(obsRec("fired"));
110
+ return true;
111
+ }
112
+ switch (event.mode) {
113
+ case "parallel": {
114
+ const settled = await Promise.allSettled(handlers.map(({ handler }) => Promise.resolve().then(() => handler(payload))));
115
+ for (const r of settled) {
116
+ if (r.status === "rejected") {
117
+ this.logger.error?.("framework-event handler threw (parallel)", {
118
+ event: event.name,
119
+ error: r.reason?.message ?? String(r.reason),
120
+ });
121
+ }
122
+ }
123
+ this.notify(obsRec("fired"));
124
+ return true;
125
+ }
126
+ case "series": {
127
+ for (const { handler } of handlers) {
128
+ await handler(payload);
129
+ }
130
+ this.notify(obsRec("fired"));
131
+ return true;
132
+ }
133
+ case "series-bail": {
134
+ for (const { handler } of handlers) {
135
+ const result = await handler(payload);
136
+ if (result === false) {
137
+ this.logger.info?.("framework-event prevented by handler", {
138
+ event: event.name,
139
+ });
140
+ this.notify(obsRec("prevented"));
141
+ return false;
142
+ }
143
+ }
144
+ this.notify(obsRec("fired"));
145
+ return true;
146
+ }
147
+ }
148
+ }
149
+ /** Count subscribers for an event — diagnostic surface (Studio, tests). */
150
+ subscriberCount(event) {
151
+ return this.subs.get(event.name)?.length ?? 0;
152
+ }
153
+ }
154
+ //# sourceMappingURL=framework-event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework-event-bus.js","sourceRoot":"","sources":["../src/framework-event-bus.ts"],"names":[],"mappings":"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"}