@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/__tests__/define-plugin.test.d.ts +16 -0
- package/dist/__tests__/define-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/define-plugin.test.js +269 -0
- package/dist/__tests__/define-plugin.test.js.map +1 -0
- package/dist/__tests__/framework-events.test.d.ts +18 -0
- package/dist/__tests__/framework-events.test.d.ts.map +1 -0
- package/dist/__tests__/framework-events.test.js +156 -0
- package/dist/__tests__/framework-events.test.js.map +1 -0
- package/dist/app.d.ts +25 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +25 -0
- package/dist/app.js.map +1 -0
- package/dist/define-plugin.d.ts +150 -0
- package/dist/define-plugin.d.ts.map +1 -0
- package/dist/define-plugin.js +72 -0
- package/dist/define-plugin.js.map +1 -0
- package/dist/framework-event-bus.d.ts +89 -0
- package/dist/framework-event-bus.d.ts.map +1 -0
- package/dist/framework-event-bus.js +154 -0
- package/dist/framework-event-bus.js.map +1 -0
- package/dist/framework-events.d.ts +217 -0
- package/dist/framework-events.d.ts.map +1 -0
- package/dist/framework-events.js +142 -0
- package/dist/framework-events.js.map +1 -0
- package/package.json +43 -0
|
@@ -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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"}
|