@nwire/app 0.12.0 → 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.
- package/dist/app-hooks.d.ts +96 -0
- package/dist/app-hooks.js +49 -0
- package/dist/app.d.ts +4 -2
- package/dist/app.js +6 -2
- package/dist/compose-app.js +2 -0
- package/dist/create-app.d.ts +16 -1
- package/dist/create-app.js +21 -2
- package/dist/define-plugin.d.ts +2 -7
- package/dist/define-plugin.js +1 -4
- package/dist/lifecycle.d.ts +45 -1
- package/dist/lifecycle.js +110 -12
- package/dist/publish.js +2 -2
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/package.json +11 -10
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App / plugin LIFECYCLE hook registry — an App concern.
|
|
3
|
+
*
|
|
4
|
+
* The Runtime is the dispatch substrate; it does not know about plugins or a
|
|
5
|
+
* boot/shutdown lifecycle. The App owns that, so the lifecycle slots
|
|
6
|
+
* (`App*` / `Plugin*`) live here, on a per-App `AppHooks` registry that
|
|
7
|
+
* `AppLifecycle` fires from. Each slot is a `Hook<TPayload>` accessed as a
|
|
8
|
+
* typed property on the registry.
|
|
9
|
+
*
|
|
10
|
+
* The slots are declared by augmenting the `AppHooks` interface exported from
|
|
11
|
+
* `@nwire/runtime` (the same forward-declaration pattern forge uses for the
|
|
12
|
+
* runtime's `FrameworkHooks`), so `PluginContext.on` / `PluginContext.defineHook`
|
|
13
|
+
* stay typed against them.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle hooks still emit `hook.step` telemetry: `AppLifecycle` wires every
|
|
16
|
+
* slot through `runtime.observe(...)`, the same tap the runtime's own hooks use,
|
|
17
|
+
* so Studio's plugin live-tap (`hook.step`) keeps working.
|
|
18
|
+
*/
|
|
19
|
+
import { type Hook } from "@nwire/hooks";
|
|
20
|
+
import type { PluginKind } from "@nwire/runtime";
|
|
21
|
+
declare module "@nwire/runtime" {
|
|
22
|
+
interface AppHooks {
|
|
23
|
+
AppRegistering: Hook<{
|
|
24
|
+
readonly appName: string;
|
|
25
|
+
}>;
|
|
26
|
+
AppBooting: Hook<{
|
|
27
|
+
readonly appName: string;
|
|
28
|
+
}>;
|
|
29
|
+
AppBooted: Hook<{
|
|
30
|
+
readonly appName: string;
|
|
31
|
+
readonly bootedAt: string;
|
|
32
|
+
}>;
|
|
33
|
+
AppReady: Hook<{
|
|
34
|
+
readonly appName: string;
|
|
35
|
+
readonly readyAt: string;
|
|
36
|
+
}>;
|
|
37
|
+
AppShuttingDown: Hook<{
|
|
38
|
+
readonly appName: string;
|
|
39
|
+
readonly reason?: string;
|
|
40
|
+
}>;
|
|
41
|
+
AppShutdown: Hook<{
|
|
42
|
+
readonly appName: string;
|
|
43
|
+
}>;
|
|
44
|
+
PluginRegistered: Hook<{
|
|
45
|
+
readonly appName: string;
|
|
46
|
+
readonly pluginName: string;
|
|
47
|
+
readonly kind?: PluginKind;
|
|
48
|
+
}>;
|
|
49
|
+
PluginBooting: Hook<{
|
|
50
|
+
readonly appName: string;
|
|
51
|
+
readonly pluginName: string;
|
|
52
|
+
readonly kind?: PluginKind;
|
|
53
|
+
}>;
|
|
54
|
+
PluginBooted: Hook<{
|
|
55
|
+
readonly appName: string;
|
|
56
|
+
readonly pluginName: string;
|
|
57
|
+
readonly durationMs: number;
|
|
58
|
+
readonly kind?: PluginKind;
|
|
59
|
+
}>;
|
|
60
|
+
PluginShuttingDown: Hook<{
|
|
61
|
+
readonly appName: string;
|
|
62
|
+
readonly pluginName: string;
|
|
63
|
+
readonly kind?: PluginKind;
|
|
64
|
+
}>;
|
|
65
|
+
PluginShutdown: Hook<{
|
|
66
|
+
readonly appName: string;
|
|
67
|
+
readonly pluginName: string;
|
|
68
|
+
readonly durationMs: number;
|
|
69
|
+
readonly kind?: PluginKind;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export type { AppHooks } from "@nwire/runtime";
|
|
74
|
+
/** Canonical names for the built-in lifecycle slots — labels the underlying Hooks. */
|
|
75
|
+
declare const BUILT_IN_APP_HOOK_NAMES: {
|
|
76
|
+
readonly AppRegistering: "nwire.app.registering";
|
|
77
|
+
readonly AppBooting: "nwire.app.booting";
|
|
78
|
+
readonly AppBooted: "nwire.app.booted";
|
|
79
|
+
readonly AppReady: "nwire.app.ready";
|
|
80
|
+
readonly AppShuttingDown: "nwire.app.shutting-down";
|
|
81
|
+
readonly AppShutdown: "nwire.app.shutdown";
|
|
82
|
+
readonly PluginRegistered: "nwire.plugin.registered";
|
|
83
|
+
readonly PluginBooting: "nwire.plugin.booting";
|
|
84
|
+
readonly PluginBooted: "nwire.plugin.booted";
|
|
85
|
+
readonly PluginShuttingDown: "nwire.plugin.shutting-down";
|
|
86
|
+
readonly PluginShutdown: "nwire.plugin.shutdown";
|
|
87
|
+
};
|
|
88
|
+
export type AppHookSlot = keyof typeof BUILT_IN_APP_HOOK_NAMES;
|
|
89
|
+
/**
|
|
90
|
+
* Construct a per-App lifecycle registry with every built-in slot
|
|
91
|
+
* pre-instantiated. Plugin-defined lifecycle slots added via
|
|
92
|
+
* `ctx.defineHook(name)` land on the same registry instance.
|
|
93
|
+
*/
|
|
94
|
+
export declare function createAppHooks(): import("@nwire/runtime").AppHooks;
|
|
95
|
+
/** True if `slot` is one of the built-in app lifecycle hook keys. */
|
|
96
|
+
export declare function isAppHook(slot: string): slot is AppHookSlot;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App / plugin LIFECYCLE hook registry — an App concern.
|
|
3
|
+
*
|
|
4
|
+
* The Runtime is the dispatch substrate; it does not know about plugins or a
|
|
5
|
+
* boot/shutdown lifecycle. The App owns that, so the lifecycle slots
|
|
6
|
+
* (`App*` / `Plugin*`) live here, on a per-App `AppHooks` registry that
|
|
7
|
+
* `AppLifecycle` fires from. Each slot is a `Hook<TPayload>` accessed as a
|
|
8
|
+
* typed property on the registry.
|
|
9
|
+
*
|
|
10
|
+
* The slots are declared by augmenting the `AppHooks` interface exported from
|
|
11
|
+
* `@nwire/runtime` (the same forward-declaration pattern forge uses for the
|
|
12
|
+
* runtime's `FrameworkHooks`), so `PluginContext.on` / `PluginContext.defineHook`
|
|
13
|
+
* stay typed against them.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle hooks still emit `hook.step` telemetry: `AppLifecycle` wires every
|
|
16
|
+
* slot through `runtime.observe(...)`, the same tap the runtime's own hooks use,
|
|
17
|
+
* so Studio's plugin live-tap (`hook.step`) keeps working.
|
|
18
|
+
*/
|
|
19
|
+
import { hook } from "@nwire/hooks";
|
|
20
|
+
/** Canonical names for the built-in lifecycle slots — labels the underlying Hooks. */
|
|
21
|
+
const BUILT_IN_APP_HOOK_NAMES = {
|
|
22
|
+
AppRegistering: "nwire.app.registering",
|
|
23
|
+
AppBooting: "nwire.app.booting",
|
|
24
|
+
AppBooted: "nwire.app.booted",
|
|
25
|
+
AppReady: "nwire.app.ready",
|
|
26
|
+
AppShuttingDown: "nwire.app.shutting-down",
|
|
27
|
+
AppShutdown: "nwire.app.shutdown",
|
|
28
|
+
PluginRegistered: "nwire.plugin.registered",
|
|
29
|
+
PluginBooting: "nwire.plugin.booting",
|
|
30
|
+
PluginBooted: "nwire.plugin.booted",
|
|
31
|
+
PluginShuttingDown: "nwire.plugin.shutting-down",
|
|
32
|
+
PluginShutdown: "nwire.plugin.shutdown",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Construct a per-App lifecycle registry with every built-in slot
|
|
36
|
+
* pre-instantiated. Plugin-defined lifecycle slots added via
|
|
37
|
+
* `ctx.defineHook(name)` land on the same registry instance.
|
|
38
|
+
*/
|
|
39
|
+
export function createAppHooks() {
|
|
40
|
+
const registry = {};
|
|
41
|
+
for (const [slot, name] of Object.entries(BUILT_IN_APP_HOOK_NAMES)) {
|
|
42
|
+
registry[slot] = hook(name);
|
|
43
|
+
}
|
|
44
|
+
return registry;
|
|
45
|
+
}
|
|
46
|
+
/** True if `slot` is one of the built-in app lifecycle hook keys. */
|
|
47
|
+
export function isAppHook(slot) {
|
|
48
|
+
return slot in BUILT_IN_APP_HOOK_NAMES;
|
|
49
|
+
}
|
package/dist/app.d.ts
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* capability set. `createRuntime` exposes the raw substrate for cases that
|
|
5
5
|
* need the dispatch core without the App layer on top.
|
|
6
6
|
*/
|
|
7
|
-
export { Runtime, createRuntime, serializeError, type DispatchHookCtx, type DispatchMiddleware, type HookStepTelemetry, type RuntimeOptions, type SerializedError, type Telemetry, type TelemetryListener, type FrameworkHooks, type PluginKind, type PluginDefinition, type PluginContext, } from "./runtime/index.js";
|
|
7
|
+
export { Runtime, createRuntime, serializeError, messageRef, type MessageRef, type DispatchHookCtx, type DispatchMiddleware, type HookStepTelemetry, type RuntimeOptions, type SerializedError, type Telemetry, type TelemetryListener, type FrameworkHooks, type PluginKind, type PluginDefinition, type PluginContext, } from "./runtime/index.js";
|
|
8
|
+
export { createAppHooks, isAppHook, type AppHooks, type AppHookSlot } from "./app-hooks.js";
|
|
8
9
|
export { definePlugin, isPlugin } from "./define-plugin.js";
|
|
9
10
|
export { createApp, type App, type CreateAppOptions } from "./create-app.js";
|
|
11
|
+
export type { PluginContributions } from "./lifecycle.js";
|
|
10
12
|
export { appCompose, containerOf, type AppComposeOptions, type CollisionPolicy, } from "./compose-app.js";
|
|
11
13
|
export { subscribe, isSubscription, type Subscription, type WhenFn } from "./subscribe.js";
|
|
12
14
|
export { publishCapability, publishPlugin, type PublishCaps } from "./publish.js";
|
|
13
|
-
export { defineCapability, type Capability, type CapabilityBase, type OutboundStage, type StagePosition, type StageContext, } from "@nwire/runtime";
|
|
15
|
+
export { defineCapability, type Capability, type CapabilityBase, type OutboundStage, type StagePosition, type StageContext, installTelemetryReporter, type TelemetryReporter, } from "@nwire/runtime";
|
|
14
16
|
export type { HandlerBaseCtx, Ctx } from "@nwire/handler";
|
package/dist/app.js
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* capability set. `createRuntime` exposes the raw substrate for cases that
|
|
5
5
|
* need the dispatch core without the App layer on top.
|
|
6
6
|
*/
|
|
7
|
-
export { Runtime, createRuntime, serializeError, } from "./runtime/index.js";
|
|
7
|
+
export { Runtime, createRuntime, serializeError, messageRef, } from "./runtime/index.js";
|
|
8
|
+
// App / plugin lifecycle hook registry — App-owned (the runtime has no
|
|
9
|
+
// lifecycle). Importing this module also applies the `AppHooks` augmentation
|
|
10
|
+
// that declares the App* / Plugin* slots.
|
|
11
|
+
export { createAppHooks, isAppHook } from "./app-hooks.js";
|
|
8
12
|
export { definePlugin, isPlugin } from "./define-plugin.js";
|
|
9
13
|
export { createApp } from "./create-app.js";
|
|
10
14
|
export { appCompose, containerOf, } from "./compose-app.js";
|
|
@@ -17,4 +21,4 @@ export { subscribe, isSubscription } from "./subscribe.js";
|
|
|
17
21
|
export { publishCapability, publishPlugin } from "./publish.js";
|
|
18
22
|
// Capability + sink primitives — re-exported from @nwire/runtime so the
|
|
19
23
|
// App surface presents a single import surface.
|
|
20
|
-
export { defineCapability, } from "@nwire/runtime";
|
|
24
|
+
export { defineCapability, installTelemetryReporter, } from "@nwire/runtime";
|
package/dist/compose-app.js
CHANGED
|
@@ -90,8 +90,10 @@ export function appCompose(...args) {
|
|
|
90
90
|
},
|
|
91
91
|
container: head.container,
|
|
92
92
|
runtime: head.runtime,
|
|
93
|
+
appHooks: head.appHooks,
|
|
93
94
|
interface: mergedInterface,
|
|
94
95
|
plugins: apps.flatMap((a) => a.plugins),
|
|
96
|
+
pluginContributions: () => apps.flatMap((a) => a.pluginContributions()),
|
|
95
97
|
with(plugin) {
|
|
96
98
|
// Composites install through the head app — multi-runtime install is
|
|
97
99
|
// out of scope. The composite's runtime is the head's runtime, so
|
package/dist/create-app.d.ts
CHANGED
|
@@ -21,8 +21,9 @@ import { type Container } from "@nwire/container/awilix";
|
|
|
21
21
|
import { type Logger } from "@nwire/logger";
|
|
22
22
|
import { type Binding, type HandlerDef, type Interface, type WireCtxBuilder } from "@nwire/wires";
|
|
23
23
|
import type { EventDefinition, EventPayload } from "@nwire/messages";
|
|
24
|
-
import type { ListenerContext } from "@nwire/runtime";
|
|
24
|
+
import type { AppHooks, ListenerContext } from "@nwire/runtime";
|
|
25
25
|
import { type PluginDefinition, type Runtime } from "./runtime/index.js";
|
|
26
|
+
import { type PluginContributions } from "./lifecycle.js";
|
|
26
27
|
import { type Subscription } from "./subscribe.js";
|
|
27
28
|
export interface CreateAppOptions {
|
|
28
29
|
/** App name — surfaces in framework-hook payloads + dev logger output. */
|
|
@@ -60,6 +61,13 @@ export interface App<TCaps = {}> {
|
|
|
60
61
|
readonly name: string;
|
|
61
62
|
readonly container: Container;
|
|
62
63
|
readonly runtime: Runtime;
|
|
64
|
+
/**
|
|
65
|
+
* App / plugin lifecycle hook registry (`App*` / `Plugin*` slots). Owned by
|
|
66
|
+
* the App, not the runtime — the runtime is a stateless dispatch substrate.
|
|
67
|
+
* Plugins observe lifecycle via `ctx.on(...)`; advanced callers can `.use()`
|
|
68
|
+
* a chain step (e.g. veto shutdown) directly on these slots.
|
|
69
|
+
*/
|
|
70
|
+
readonly appHooks: AppHooks;
|
|
63
71
|
/**
|
|
64
72
|
* Wire collection — the same shape as `createInterface()` standalone.
|
|
65
73
|
* Adopters consume `app.interface.forAdapter(kind)`; foundation-form
|
|
@@ -67,6 +75,13 @@ export interface App<TCaps = {}> {
|
|
|
67
75
|
*/
|
|
68
76
|
readonly interface: Interface;
|
|
69
77
|
readonly plugins: readonly PluginDefinition[];
|
|
78
|
+
/**
|
|
79
|
+
* Per-plugin contribution attribution captured at boot — which DI bindings,
|
|
80
|
+
* capabilities, stages, handlers, and hooks each plugin added. Empty until
|
|
81
|
+
* the plugin phases have run. Read by `captureTopology` to emit the
|
|
82
|
+
* `contributes` graph edge (Studio's Plugins page).
|
|
83
|
+
*/
|
|
84
|
+
pluginContributions(): readonly PluginContributions[];
|
|
70
85
|
/** Phantom marker — never read at runtime. */
|
|
71
86
|
readonly __caps?: TCaps;
|
|
72
87
|
/**
|
package/dist/create-app.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { createContainer } from "@nwire/container/awilix";
|
|
21
21
|
import { NoopLogger } from "@nwire/logger";
|
|
22
22
|
import { createInterface, } from "@nwire/wires";
|
|
23
|
+
import { autoInstallFileReporter, installTopologyEmit } from "@nwire/telemetry";
|
|
23
24
|
import { createRuntime } from "./runtime/index.js";
|
|
24
25
|
import { AppLifecycle } from "./lifecycle.js";
|
|
25
26
|
import { isSubscription } from "./subscribe.js";
|
|
@@ -42,6 +43,15 @@ export function createApp(options) {
|
|
|
42
43
|
for (const p of plugins) {
|
|
43
44
|
lifecycle.enqueue(p);
|
|
44
45
|
}
|
|
46
|
+
// Dev telemetry persistence — userland-unaware. When the process runs inside
|
|
47
|
+
// a project (a `.nwire` dir or `NWIRE_CWD` set), install the local file
|
|
48
|
+
// reporter so every run lands in `.nwire/telemetry/*.jsonl` for Studio
|
|
49
|
+
// history. No-op in unit tests / library imports (no project cwd). The
|
|
50
|
+
// detach runs at shutdown so the stream closes cleanly.
|
|
51
|
+
const detachTelemetry = autoInstallFileReporter(runtime);
|
|
52
|
+
if (detachTelemetry) {
|
|
53
|
+
lifecycle.appHooks.AppShutdown.on(() => detachTelemetry());
|
|
54
|
+
}
|
|
45
55
|
const app = {
|
|
46
56
|
$nwireApp: true,
|
|
47
57
|
$kind: "app",
|
|
@@ -49,8 +59,10 @@ export function createApp(options) {
|
|
|
49
59
|
name: appName,
|
|
50
60
|
container,
|
|
51
61
|
runtime,
|
|
62
|
+
appHooks: lifecycle.appHooks,
|
|
52
63
|
interface: iface,
|
|
53
64
|
plugins,
|
|
65
|
+
pluginContributions: () => lifecycle.pluginContributions(),
|
|
54
66
|
get state() {
|
|
55
67
|
return lifecycle.currentState;
|
|
56
68
|
},
|
|
@@ -99,13 +111,20 @@ export function createApp(options) {
|
|
|
99
111
|
boot: (appConfig) => app.start(appConfig),
|
|
100
112
|
shutdown: () => lifecycle.stop(),
|
|
101
113
|
dispatchFrameworkEvent: async (slot, payload) => {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
114
|
+
const appReg = lifecycle.appHooks;
|
|
115
|
+
const rtReg = runtime.hooks;
|
|
116
|
+
const h = appReg[slot] ?? rtReg[slot];
|
|
104
117
|
if (!h)
|
|
105
118
|
return true;
|
|
106
119
|
const result = await h.runDetailed(payload);
|
|
107
120
|
return result.outcome === "completed";
|
|
108
121
|
},
|
|
109
122
|
};
|
|
123
|
+
// Topology self-emit — userland-unaware, same project-cwd gate as the
|
|
124
|
+
// telemetry auto-install. When the app boots inside a project, it captures its
|
|
125
|
+
// own runtime wiring on `AppReady` and writes `.nwire/topology.json` so the
|
|
126
|
+
// static scanner can fold it in without booting the app itself. The file is
|
|
127
|
+
// removed on shutdown. No-op in unit tests / library imports (no project cwd).
|
|
128
|
+
installTopologyEmit(app);
|
|
110
129
|
return app;
|
|
111
130
|
}
|
package/dist/define-plugin.d.ts
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
* Both produce a `PluginDefinition<TOptions, TMark>` that the Runtime's
|
|
19
19
|
* `registerPlugin` runs as register-then-setup.
|
|
20
20
|
*/
|
|
21
|
-
import { type SourceLocation } from "@nwire/messages";
|
|
22
21
|
import type { PluginContext, PluginDefinition } from "./runtime/index.js";
|
|
23
22
|
/** Body for the object call form. */
|
|
24
23
|
export interface DefinePluginBody<TOptions = void, TMark = unknown> {
|
|
@@ -28,12 +27,8 @@ export interface DefinePluginBody<TOptions = void, TMark = unknown> {
|
|
|
28
27
|
readonly __mark?: TMark;
|
|
29
28
|
}
|
|
30
29
|
/** Single-phase form — receives a setup closure directly. */
|
|
31
|
-
export declare function definePlugin(name: string, setup: (ctx: PluginContext) => void): PluginDefinition
|
|
32
|
-
readonly $source?: SourceLocation;
|
|
33
|
-
};
|
|
30
|
+
export declare function definePlugin(name: string, setup: (ctx: PluginContext) => void): PluginDefinition;
|
|
34
31
|
/** Two-phase form — accepts options + register + setup as a body object. */
|
|
35
|
-
export declare function definePlugin<TMark = unknown, TOptions = void>(name: string, body: DefinePluginBody<TOptions, TMark>): PluginDefinition<TOptions, TMark
|
|
36
|
-
readonly $source?: SourceLocation;
|
|
37
|
-
};
|
|
32
|
+
export declare function definePlugin<TMark = unknown, TOptions = void>(name: string, body: DefinePluginBody<TOptions, TMark>): PluginDefinition<TOptions, TMark>;
|
|
38
33
|
/** Discriminator for runtime-side checks. */
|
|
39
34
|
export declare function isPlugin(x: unknown): x is PluginDefinition;
|
package/dist/define-plugin.js
CHANGED
|
@@ -18,12 +18,10 @@
|
|
|
18
18
|
* Both produce a `PluginDefinition<TOptions, TMark>` that the Runtime's
|
|
19
19
|
* `registerPlugin` runs as register-then-setup.
|
|
20
20
|
*/
|
|
21
|
-
import { captureSourceLocation } from "@nwire/messages";
|
|
22
21
|
// ─── Implementation ───────────────────────────────────────────────────────
|
|
23
22
|
export function definePlugin(name, setupOrBody) {
|
|
24
|
-
const $source = captureSourceLocation();
|
|
25
23
|
if (typeof setupOrBody === "function") {
|
|
26
|
-
return { name, setup: setupOrBody
|
|
24
|
+
return { name, setup: setupOrBody };
|
|
27
25
|
}
|
|
28
26
|
return {
|
|
29
27
|
name,
|
|
@@ -31,7 +29,6 @@ export function definePlugin(name, setupOrBody) {
|
|
|
31
29
|
register: setupOrBody.register,
|
|
32
30
|
setup: setupOrBody.setup,
|
|
33
31
|
__mark: setupOrBody.__mark,
|
|
34
|
-
$source,
|
|
35
32
|
};
|
|
36
33
|
}
|
|
37
34
|
/** Discriminator for runtime-side checks. */
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -8,7 +8,23 @@
|
|
|
8
8
|
* App composes the Runtime with this orchestrator to get plugin lifecycle.
|
|
9
9
|
*/
|
|
10
10
|
import type { Container } from "@nwire/container";
|
|
11
|
-
import type { PluginDefinition, Runtime } from "@nwire/runtime";
|
|
11
|
+
import type { AppHooks, PluginDefinition, Runtime } from "@nwire/runtime";
|
|
12
|
+
/**
|
|
13
|
+
* What a plugin contributed to the app during its register + setup phases:
|
|
14
|
+
* DI bindings, capabilities, source/sink stages, registered handlers, and
|
|
15
|
+
* lifecycle hooks. Captured by diffing the runtime + global hook registry
|
|
16
|
+
* around each plugin's phase, so Studio can attribute every topology entry to
|
|
17
|
+
* the plugin that added it (the `contributes` graph edge).
|
|
18
|
+
*/
|
|
19
|
+
export interface PluginContributions {
|
|
20
|
+
readonly plugin: string;
|
|
21
|
+
readonly bindings: readonly string[];
|
|
22
|
+
readonly capabilities: readonly string[];
|
|
23
|
+
readonly sourceStages: readonly string[];
|
|
24
|
+
readonly sinkStages: readonly string[];
|
|
25
|
+
readonly handlers: readonly string[];
|
|
26
|
+
readonly hooks: readonly string[];
|
|
27
|
+
}
|
|
12
28
|
/** Lifecycle state. */
|
|
13
29
|
export type LifecycleState = "idle" | "starting" | "running" | "stopping" | "stopped";
|
|
14
30
|
export interface AppLifecycleOptions {
|
|
@@ -23,16 +39,27 @@ export declare class AppLifecycle {
|
|
|
23
39
|
private readonly appName;
|
|
24
40
|
private readonly container;
|
|
25
41
|
private readonly runtime;
|
|
42
|
+
/**
|
|
43
|
+
* App / plugin lifecycle hook registry. Owned by the App, not the runtime —
|
|
44
|
+
* the runtime has no lifecycle. Wired through `runtime.observe(...)` so each
|
|
45
|
+
* slot emits `hook.step` telemetry exactly like the runtime's own hooks
|
|
46
|
+
* (Studio's plugin live-tap consumes these).
|
|
47
|
+
*/
|
|
48
|
+
private readonly hooks;
|
|
26
49
|
/** Plugins enqueued via `enqueue` whose phases haven't run yet. */
|
|
27
50
|
private readonly pendingPlugins;
|
|
28
51
|
/** Plugins whose register + setup have run. Ordered by registration. */
|
|
29
52
|
private readonly plugins;
|
|
53
|
+
/** What each plugin contributed, captured during `runPluginPhases`. */
|
|
54
|
+
private readonly contributions;
|
|
30
55
|
private state;
|
|
31
56
|
private startPromise;
|
|
32
57
|
private stopPromise;
|
|
33
58
|
constructor(opts: AppLifecycleOptions);
|
|
34
59
|
/** Current lifecycle phase. */
|
|
35
60
|
get currentState(): LifecycleState;
|
|
61
|
+
/** The app's lifecycle hook registry (App* / Plugin* slots). */
|
|
62
|
+
get appHooks(): AppHooks;
|
|
36
63
|
/** Names of every plugin queued or registered, in registration order. */
|
|
37
64
|
pluginNames(): readonly string[];
|
|
38
65
|
/**
|
|
@@ -47,6 +74,17 @@ export declare class AppLifecycle {
|
|
|
47
74
|
* idempotent (no-op once the queue is empty).
|
|
48
75
|
*/
|
|
49
76
|
runPluginPhases(): void;
|
|
77
|
+
/** A snapshot of the app's current registered surface — for contribution diffs. */
|
|
78
|
+
private snapshot;
|
|
79
|
+
/** Global hook registry names — best-effort (tolerate an absent registry). */
|
|
80
|
+
private hookNames;
|
|
81
|
+
/**
|
|
82
|
+
* Fold the surface added since `before` into the named plugin's contribution
|
|
83
|
+
* record, merging across the register + setup phases.
|
|
84
|
+
*/
|
|
85
|
+
private recordContribution;
|
|
86
|
+
/** Per-plugin contribution attribution — read by `captureTopology`. */
|
|
87
|
+
pluginContributions(): readonly PluginContributions[];
|
|
50
88
|
/**
|
|
51
89
|
* Boot the app:
|
|
52
90
|
*
|
|
@@ -76,4 +114,10 @@ export declare class AppLifecycle {
|
|
|
76
114
|
stop(reason?: string): Promise<void>;
|
|
77
115
|
/** Build the plugin context — same shape across register and setup. */
|
|
78
116
|
private buildPluginContext;
|
|
117
|
+
/**
|
|
118
|
+
* Materialise a plugin-defined slot on the app lifecycle registry.
|
|
119
|
+
* Idempotent; the new slot is observed for `hook.step` telemetry like the
|
|
120
|
+
* built-in ones. Mirrors `Runtime.defineHook` but for app hooks.
|
|
121
|
+
*/
|
|
122
|
+
private defineAppHook;
|
|
79
123
|
}
|
package/dist/lifecycle.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* events, middleware, telemetry). It does not know about plugins. The
|
|
8
8
|
* App composes the Runtime with this orchestrator to get plugin lifecycle.
|
|
9
9
|
*/
|
|
10
|
+
import { hook as hookFactory, listHooks } from "@nwire/hooks";
|
|
11
|
+
import { createAppHooks } from "./app-hooks.js";
|
|
10
12
|
/**
|
|
11
13
|
* The orchestrator. Each App owns one of these.
|
|
12
14
|
*/
|
|
@@ -14,10 +16,19 @@ export class AppLifecycle {
|
|
|
14
16
|
appName;
|
|
15
17
|
container;
|
|
16
18
|
runtime;
|
|
19
|
+
/**
|
|
20
|
+
* App / plugin lifecycle hook registry. Owned by the App, not the runtime —
|
|
21
|
+
* the runtime has no lifecycle. Wired through `runtime.observe(...)` so each
|
|
22
|
+
* slot emits `hook.step` telemetry exactly like the runtime's own hooks
|
|
23
|
+
* (Studio's plugin live-tap consumes these).
|
|
24
|
+
*/
|
|
25
|
+
hooks = createAppHooks();
|
|
17
26
|
/** Plugins enqueued via `enqueue` whose phases haven't run yet. */
|
|
18
27
|
pendingPlugins = [];
|
|
19
28
|
/** Plugins whose register + setup have run. Ordered by registration. */
|
|
20
29
|
plugins = [];
|
|
30
|
+
/** What each plugin contributed, captured during `runPluginPhases`. */
|
|
31
|
+
contributions = [];
|
|
21
32
|
state = "idle";
|
|
22
33
|
startPromise;
|
|
23
34
|
stopPromise;
|
|
@@ -25,11 +36,21 @@ export class AppLifecycle {
|
|
|
25
36
|
this.appName = opts.appName;
|
|
26
37
|
this.container = opts.container;
|
|
27
38
|
this.runtime = opts.runtime;
|
|
39
|
+
// Route every lifecycle hook's step telemetry through the runtime's
|
|
40
|
+
// canonical stream — same tap the runtime's own hooks use — so the
|
|
41
|
+
// `hook.step` records Studio's plugin live-tap relies on keep flowing.
|
|
42
|
+
for (const h of Object.values(this.hooks)) {
|
|
43
|
+
this.runtime.observe(h);
|
|
44
|
+
}
|
|
28
45
|
}
|
|
29
46
|
/** Current lifecycle phase. */
|
|
30
47
|
get currentState() {
|
|
31
48
|
return this.state;
|
|
32
49
|
}
|
|
50
|
+
/** The app's lifecycle hook registry (App* / Plugin* slots). */
|
|
51
|
+
get appHooks() {
|
|
52
|
+
return this.hooks;
|
|
53
|
+
}
|
|
33
54
|
/** Names of every plugin queued or registered, in registration order. */
|
|
34
55
|
pluginNames() {
|
|
35
56
|
return [...this.plugins.map((p) => p.name), ...this.pendingPlugins.map((p) => p.def.name)];
|
|
@@ -66,17 +87,85 @@ export class AppLifecycle {
|
|
|
66
87
|
const ctx = this.buildPluginContext(p.def, boots, disposes);
|
|
67
88
|
return { def: p.def, ctx, boots, disposes };
|
|
68
89
|
});
|
|
90
|
+
// Attribute every binding/capability/stage/handler/hook a plugin adds by
|
|
91
|
+
// diffing the runtime + hook registry around each of its phases. Snapshot
|
|
92
|
+
// immediately before each call so the diff is that one plugin's delta —
|
|
93
|
+
// even though all registers run before all setups.
|
|
69
94
|
for (const p of prepared) {
|
|
70
|
-
if (p.def.register)
|
|
95
|
+
if (p.def.register) {
|
|
96
|
+
const before = this.snapshot();
|
|
71
97
|
p.def.register(p.ctx);
|
|
98
|
+
this.recordContribution(p.def.name, before);
|
|
99
|
+
}
|
|
72
100
|
}
|
|
73
101
|
for (const p of prepared) {
|
|
102
|
+
const before = this.snapshot();
|
|
74
103
|
p.def.setup(p.ctx);
|
|
104
|
+
this.recordContribution(p.def.name, before);
|
|
75
105
|
}
|
|
76
106
|
for (const p of prepared) {
|
|
77
107
|
this.plugins.push({ name: p.def.name, boots: p.boots, disposes: p.disposes });
|
|
78
108
|
}
|
|
79
109
|
}
|
|
110
|
+
/** A snapshot of the app's current registered surface — for contribution diffs. */
|
|
111
|
+
snapshot() {
|
|
112
|
+
const rt = this.runtime;
|
|
113
|
+
const names = (xs, key) => new Set((xs ?? []).map(key));
|
|
114
|
+
return {
|
|
115
|
+
bindings: names(this.container.list?.(), (b) => b.name),
|
|
116
|
+
capabilities: new Set(rt.listCapabilities?.() ?? []),
|
|
117
|
+
sourceStages: names(rt.listSourceStages?.(), (s) => s.name),
|
|
118
|
+
sinkStages: names(rt.listSinkStages?.(), (s) => s.name),
|
|
119
|
+
handlers: new Set(rt.listHandlers?.() ?? []),
|
|
120
|
+
hooks: this.hookNames(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Global hook registry names — best-effort (tolerate an absent registry). */
|
|
124
|
+
hookNames() {
|
|
125
|
+
try {
|
|
126
|
+
return new Set(listHooks().map((h) => h.name));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return new Set();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Fold the surface added since `before` into the named plugin's contribution
|
|
134
|
+
* record, merging across the register + setup phases.
|
|
135
|
+
*/
|
|
136
|
+
recordContribution(plugin, before) {
|
|
137
|
+
const after = this.snapshot();
|
|
138
|
+
const added = (a, b) => [...b].filter((x) => !a.has(x));
|
|
139
|
+
const delta = {
|
|
140
|
+
plugin,
|
|
141
|
+
bindings: added(before.bindings, after.bindings),
|
|
142
|
+
capabilities: added(before.capabilities, after.capabilities),
|
|
143
|
+
sourceStages: added(before.sourceStages, after.sourceStages),
|
|
144
|
+
sinkStages: added(before.sinkStages, after.sinkStages),
|
|
145
|
+
handlers: added(before.handlers, after.handlers),
|
|
146
|
+
hooks: added(before.hooks, after.hooks),
|
|
147
|
+
};
|
|
148
|
+
const existing = this.contributions.find((c) => c.plugin === plugin);
|
|
149
|
+
if (!existing) {
|
|
150
|
+
this.contributions.push(delta);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const merge = (a, b) => [
|
|
154
|
+
...new Set([...a, ...b]),
|
|
155
|
+
];
|
|
156
|
+
Object.assign(existing, {
|
|
157
|
+
bindings: merge(existing.bindings, delta.bindings),
|
|
158
|
+
capabilities: merge(existing.capabilities, delta.capabilities),
|
|
159
|
+
sourceStages: merge(existing.sourceStages, delta.sourceStages),
|
|
160
|
+
sinkStages: merge(existing.sinkStages, delta.sinkStages),
|
|
161
|
+
handlers: merge(existing.handlers, delta.handlers),
|
|
162
|
+
hooks: merge(existing.hooks, delta.hooks),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Per-plugin contribution attribution — read by `captureTopology`. */
|
|
166
|
+
pluginContributions() {
|
|
167
|
+
return this.contributions;
|
|
168
|
+
}
|
|
80
169
|
/**
|
|
81
170
|
* Boot the app:
|
|
82
171
|
*
|
|
@@ -100,7 +189,7 @@ export class AppLifecycle {
|
|
|
100
189
|
}
|
|
101
190
|
this.state = "starting";
|
|
102
191
|
const appName = this.appName;
|
|
103
|
-
const hooks = this.
|
|
192
|
+
const hooks = this.hooks;
|
|
104
193
|
this.startPromise = (async () => {
|
|
105
194
|
try {
|
|
106
195
|
this.runPluginPhases();
|
|
@@ -183,7 +272,7 @@ export class AppLifecycle {
|
|
|
183
272
|
}
|
|
184
273
|
this.state = "stopping";
|
|
185
274
|
const appName = this.appName;
|
|
186
|
-
const hooks = this.
|
|
275
|
+
const hooks = this.hooks;
|
|
187
276
|
this.stopPromise = (async () => {
|
|
188
277
|
const errors = [];
|
|
189
278
|
const shuttingDown = await hooks.AppShuttingDown.runDetailed({ appName, reason });
|
|
@@ -250,17 +339,11 @@ export class AppLifecycle {
|
|
|
250
339
|
bind: (name, factory, opts) => {
|
|
251
340
|
this.container.register(name, factory, opts);
|
|
252
341
|
},
|
|
253
|
-
add: (cap) => {
|
|
254
|
-
this.runtime.add(cap);
|
|
255
|
-
},
|
|
256
|
-
sink: (stage) => {
|
|
257
|
-
this.runtime.sink(stage);
|
|
258
|
-
},
|
|
259
342
|
on: (name, fn) => {
|
|
260
|
-
const reg = this.
|
|
343
|
+
const reg = this.hooks;
|
|
261
344
|
const h = reg[name];
|
|
262
345
|
if (!h) {
|
|
263
|
-
throw new Error(`PluginContext.on:
|
|
346
|
+
throw new Error(`PluginContext.on: app lifecycle hook "${String(name)}" does not exist — call defineHook first if it's a plugin-defined slot.`);
|
|
264
347
|
}
|
|
265
348
|
h.on(fn);
|
|
266
349
|
},
|
|
@@ -273,7 +356,22 @@ export class AppLifecycle {
|
|
|
273
356
|
shutdown: (fn) => {
|
|
274
357
|
disposes.push(fn);
|
|
275
358
|
},
|
|
276
|
-
defineHook: (name) => this.
|
|
359
|
+
defineHook: (name) => this.defineAppHook(name),
|
|
277
360
|
};
|
|
278
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Materialise a plugin-defined slot on the app lifecycle registry.
|
|
364
|
+
* Idempotent; the new slot is observed for `hook.step` telemetry like the
|
|
365
|
+
* built-in ones. Mirrors `Runtime.defineHook` but for app hooks.
|
|
366
|
+
*/
|
|
367
|
+
defineAppHook(name) {
|
|
368
|
+
const reg = this.hooks;
|
|
369
|
+
const existing = reg[name];
|
|
370
|
+
if (existing)
|
|
371
|
+
return existing;
|
|
372
|
+
const h = hookFactory(`nwire.${name}`);
|
|
373
|
+
reg[name] = h;
|
|
374
|
+
this.runtime.observe(h);
|
|
375
|
+
return h;
|
|
376
|
+
}
|
|
279
377
|
}
|
package/dist/publish.js
CHANGED
|
@@ -54,8 +54,8 @@ export const publishCapability = defineCapability({
|
|
|
54
54
|
*/
|
|
55
55
|
export function publishPlugin() {
|
|
56
56
|
return definePlugin("publish", {
|
|
57
|
-
setup: ({
|
|
58
|
-
add(publishCapability);
|
|
57
|
+
setup: ({ runtime }) => {
|
|
58
|
+
runtime.add(publishCapability);
|
|
59
59
|
},
|
|
60
60
|
});
|
|
61
61
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Re-exports the canonical Runtime surface from `@nwire/runtime` so
|
|
6
6
|
* `@nwire/app` consumers can import everything from a single package.
|
|
7
7
|
*/
|
|
8
|
-
export { Runtime, createRuntime, isBuiltInHook, serializeError, type RuntimeOptions, type Telemetry, type TelemetryListener, type HookStepTelemetry, type DispatchHookCtx, type DispatchMiddleware, type SerializedError, type PluginDefinition, type PluginContext, } from "@nwire/runtime";
|
|
8
|
+
export { Runtime, createRuntime, isBuiltInHook, serializeError, messageRef, type MessageRef, type RuntimeOptions, type Telemetry, type TelemetryListener, type HookStepTelemetry, type DispatchHookCtx, type DispatchMiddleware, type SerializedError, type PluginDefinition, type PluginContext, } from "@nwire/runtime";
|
|
9
9
|
export { createFrameworkHooks, type FrameworkHooks, type PluginKind } from "@nwire/runtime";
|
package/dist/runtime/index.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Re-exports the canonical Runtime surface from `@nwire/runtime` so
|
|
6
6
|
* `@nwire/app` consumers can import everything from a single package.
|
|
7
7
|
*/
|
|
8
|
-
export { Runtime, createRuntime, isBuiltInHook, serializeError, } from "@nwire/runtime";
|
|
8
|
+
export { Runtime, createRuntime, isBuiltInHook, serializeError, messageRef, } from "@nwire/runtime";
|
|
9
9
|
export { createFrameworkHooks } from "@nwire/runtime";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nwire — managed Container with plugin lifecycle, framework
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Nwire — managed Container with plugin lifecycle, framework hooks, and DI. Composes apps + plugins, boots in order, exposes a Container, fires framework hooks at every lifecycle transition.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
7
7
|
"container",
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@nwire/container": "0.
|
|
33
|
-
"@nwire/
|
|
34
|
-
"@nwire/
|
|
35
|
-
"@nwire/
|
|
36
|
-
"@nwire/
|
|
37
|
-
"@nwire/
|
|
38
|
-
"@nwire/
|
|
39
|
-
"@nwire/
|
|
32
|
+
"@nwire/container": "0.13.0",
|
|
33
|
+
"@nwire/handler": "0.13.0",
|
|
34
|
+
"@nwire/logger": "0.13.0",
|
|
35
|
+
"@nwire/hooks": "0.13.0",
|
|
36
|
+
"@nwire/telemetry": "0.13.0",
|
|
37
|
+
"@nwire/wires": "0.13.0",
|
|
38
|
+
"@nwire/runtime": "0.13.0",
|
|
39
|
+
"@nwire/messages": "0.13.0",
|
|
40
|
+
"@nwire/envelope": "0.13.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/node": "^22.19.9",
|