@zudojs/runtime 0.1.0 → 1.1.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 +127 -6
- package/dist/dependencyGraph/dependencyGraph.core.d.ts +11 -5
- package/dist/dependencyGraph/dependencyGraph.core.js +13 -12
- package/dist/dependencyGraph/dependencyGraph.type.d.ts +0 -7
- package/dist/dependencyGraph/index.d.ts +2 -2
- package/dist/dependencyGraph/index.js +1 -1
- package/dist/health/health.core.d.ts +12 -0
- package/dist/health/health.core.js +55 -0
- package/dist/health/index.d.ts +5 -0
- package/dist/health/index.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/index.d.ts +1 -1
- package/dist/lifecycle/lifecycle.core.d.ts +41 -10
- package/dist/lifecycle/lifecycle.core.js +238 -61
- package/dist/lifecycle/lifecycle.type.d.ts +48 -1
- package/dist/readiness/index.d.ts +1 -1
- package/dist/readiness/readiness.core.d.ts +36 -4
- package/dist/readiness/readiness.core.js +122 -6
- package/dist/readiness/readiness.type.d.ts +15 -1
- package/dist/registry/registry.core.d.ts +14 -1
- package/dist/registry/registry.core.js +41 -2
- package/dist/runtime/runtime.core.d.ts +75 -5
- package/dist/runtime/runtime.core.js +210 -43
- package/dist/runtimeContext/index.d.ts +2 -2
- package/dist/runtimeContext/index.js +1 -1
- package/dist/runtimeContext/runtimeContext.core.d.ts +13 -4
- package/dist/runtimeContext/runtimeContext.core.js +22 -11
- package/dist/runtimeContext/runtimeContext.type.d.ts +12 -13
- package/dist/runtimeError/runtimeError.base.js +1 -1
- package/dist/runtimeEvents/index.d.ts +2 -2
- package/dist/runtimeEvents/index.js +1 -1
- package/dist/runtimeEvents/runtimeEvents.core.d.ts +15 -1
- package/dist/runtimeEvents/runtimeEvents.core.js +26 -0
- package/dist/runtimeEvents/runtimeEvents.type.d.ts +19 -4
- package/dist/runtimeOptions/runtimeOptions.core.d.ts +5 -0
- package/dist/runtimeOptions/runtimeOptions.core.js +14 -1
- package/dist/runtimeOptions/runtimeOptions.type.d.ts +29 -1
- package/dist/runtimeOptions/runtimeOptions.type.js +6 -0
- package/dist/runtimeState/index.d.ts +1 -1
- package/dist/runtimeState/runtimeState.core.d.ts +4 -0
- package/dist/runtimeState/runtimeState.core.js +17 -4
- package/dist/runtimeState/runtimeState.type.d.ts +14 -17
- package/dist/shutdown/shutdown.core.d.ts +20 -1
- package/dist/shutdown/shutdown.core.js +55 -29
- package/dist/signalHandler/signalHandler.core.d.ts +53 -13
- package/dist/signalHandler/signalHandler.core.js +97 -27
- package/dist/startup/startup.core.d.ts +3 -2
- package/dist/startup/startup.core.js +52 -46
- package/dist/testRuntime/index.d.ts +6 -1
- package/dist/testRuntime/index.js +5 -1
- package/dist/testRuntime/testRuntime.core.d.ts +21 -2
- package/dist/testRuntime/testRuntime.core.js +23 -6
- package/package.json +33 -22
- package/dist/.tsbuildinfo +0 -1
- package/dist/dependencyGraph/dependencyGraph.core.d.ts.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.core.js.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.type.d.ts.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.type.js.map +0 -1
- package/dist/dependencyGraph/index.d.ts.map +0 -1
- package/dist/dependencyGraph/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle/index.d.ts.map +0 -1
- package/dist/lifecycle/index.js.map +0 -1
- package/dist/lifecycle/lifecycle.core.d.ts.map +0 -1
- package/dist/lifecycle/lifecycle.core.js.map +0 -1
- package/dist/lifecycle/lifecycle.type.d.ts.map +0 -1
- package/dist/lifecycle/lifecycle.type.js.map +0 -1
- package/dist/readiness/index.d.ts.map +0 -1
- package/dist/readiness/index.js.map +0 -1
- package/dist/readiness/readiness.core.d.ts.map +0 -1
- package/dist/readiness/readiness.core.js.map +0 -1
- package/dist/readiness/readiness.type.d.ts.map +0 -1
- package/dist/readiness/readiness.type.js.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/registry/registry.core.d.ts.map +0 -1
- package/dist/registry/registry.core.js.map +0 -1
- package/dist/runtime/runtime.core.d.ts.map +0 -1
- package/dist/runtime/runtime.core.js.map +0 -1
- package/dist/runtimeContext/index.d.ts.map +0 -1
- package/dist/runtimeContext/index.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.core.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.core.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.factory.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.factory.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.type.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.type.js.map +0 -1
- package/dist/runtimeError/index.d.ts.map +0 -1
- package/dist/runtimeError/index.js.map +0 -1
- package/dist/runtimeError/runtimeError.base.d.ts.map +0 -1
- package/dist/runtimeError/runtimeError.base.js.map +0 -1
- package/dist/runtimeEvents/index.d.ts.map +0 -1
- package/dist/runtimeEvents/index.js.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.core.d.ts.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.core.js.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.type.d.ts.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.type.js.map +0 -1
- package/dist/runtimeOptions/index.d.ts.map +0 -1
- package/dist/runtimeOptions/index.js.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.core.d.ts.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.core.js.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.type.d.ts.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.type.js.map +0 -1
- package/dist/runtimeState/index.d.ts.map +0 -1
- package/dist/runtimeState/index.js.map +0 -1
- package/dist/runtimeState/runtimeState.core.d.ts.map +0 -1
- package/dist/runtimeState/runtimeState.core.js.map +0 -1
- package/dist/runtimeState/runtimeState.type.d.ts.map +0 -1
- package/dist/runtimeState/runtimeState.type.js.map +0 -1
- package/dist/shutdown/index.d.ts.map +0 -1
- package/dist/shutdown/index.js.map +0 -1
- package/dist/shutdown/shutdown.core.d.ts.map +0 -1
- package/dist/shutdown/shutdown.core.js.map +0 -1
- package/dist/signalHandler/index.d.ts.map +0 -1
- package/dist/signalHandler/index.js.map +0 -1
- package/dist/signalHandler/signalHandler.core.d.ts.map +0 -1
- package/dist/signalHandler/signalHandler.core.js.map +0 -1
- package/dist/startup/index.d.ts.map +0 -1
- package/dist/startup/index.js.map +0 -1
- package/dist/startup/startup.core.d.ts.map +0 -1
- package/dist/startup/startup.core.js.map +0 -1
- package/dist/testRuntime/index.d.ts.map +0 -1
- package/dist/testRuntime/index.js.map +0 -1
- package/dist/testRuntime/testRuntime.core.d.ts.map +0 -1
- package/dist/testRuntime/testRuntime.core.js.map +0 -1
|
@@ -1,73 +1,108 @@
|
|
|
1
|
+
import { createConfigurationManager } from "@zudojs/core";
|
|
2
|
+
import { createModuleEventPayload } from "../runtimeEvents/runtimeEvents.core.js";
|
|
1
3
|
import { resolveDependencies } from "../dependencyGraph/index.js";
|
|
2
|
-
import {
|
|
4
|
+
import { RuntimeDependencyError, RuntimeStateError, } from "../runtimeError/index.js";
|
|
3
5
|
/**
|
|
4
6
|
* Manages the lifecycle of runtime modules.
|
|
5
7
|
*/
|
|
6
8
|
export class LifecycleManager {
|
|
7
9
|
modules;
|
|
8
10
|
logger;
|
|
9
|
-
container;
|
|
10
|
-
runtimeId;
|
|
11
|
-
environment;
|
|
12
11
|
options;
|
|
12
|
+
onModuleEvent;
|
|
13
|
+
runtimeId;
|
|
13
14
|
initializedModules = [];
|
|
14
15
|
startedModules = [];
|
|
15
|
-
|
|
16
|
+
configuration;
|
|
17
|
+
application;
|
|
18
|
+
contexts = new Map();
|
|
19
|
+
constructor(modules, logger, options = {}, services = {}) {
|
|
16
20
|
this.modules = modules;
|
|
17
21
|
this.logger = logger;
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
22
|
+
this.configuration = services.configuration ?? createConfigurationManager();
|
|
23
|
+
this.application = services.application;
|
|
24
|
+
this.onModuleEvent = options.onModuleEvent;
|
|
25
|
+
this.runtimeId = options.runtimeId ?? "";
|
|
21
26
|
this.options = {
|
|
22
27
|
shutdownTimeout: options.shutdownTimeout ?? 30_000,
|
|
23
28
|
continueOnFailure: options.continueOnFailure ?? false,
|
|
24
|
-
|
|
29
|
+
// Defaults to sequential: initializing a whole depth group at once
|
|
30
|
+
// is a real behaviour change for modules that assume ordering
|
|
31
|
+
// beyond what they declared, so it is opt-in.
|
|
32
|
+
parallelInitialization: options.parallelInitialization ?? false,
|
|
25
33
|
};
|
|
26
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Publishes a per-module lifecycle event.
|
|
37
|
+
*
|
|
38
|
+
* A throwing listener must not fail the phase that produced the event,
|
|
39
|
+
* so delivery failures are contained here.
|
|
40
|
+
*/
|
|
41
|
+
emitModuleEvent(type, moduleId, state, options = {}) {
|
|
42
|
+
if (!this.onModuleEvent) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const module = this.modules.get(moduleId);
|
|
46
|
+
try {
|
|
47
|
+
this.onModuleEvent(type, createModuleEventPayload(this.runtimeId, state, moduleId, module?.name ?? moduleId, options));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.logger.warn("A runtime module event listener threw.", {
|
|
51
|
+
eventType: type,
|
|
52
|
+
moduleId,
|
|
53
|
+
error: error instanceof Error ? error.message : String(error),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Loads the configuration manager once, before modules are initialized,
|
|
59
|
+
* so that `context.getConfiguration()` is usable from the first hook.
|
|
60
|
+
*/
|
|
61
|
+
async ensureConfigurationReady() {
|
|
62
|
+
if (this.configuration.isReady()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await this.configuration.initialize();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
this.logger.warn("Configuration failed to load.", {
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
27
74
|
/**
|
|
28
75
|
* Initializes all modules in dependency order.
|
|
29
76
|
*/
|
|
30
77
|
async initialize() {
|
|
31
78
|
const startTime = Date.now();
|
|
79
|
+
// Reset the per-run bookkeeping. Appending across runs would make a
|
|
80
|
+
// stop-then-start cycle initialize and stop every module twice.
|
|
81
|
+
this.initializedModules = [];
|
|
82
|
+
this.startedModules = [];
|
|
83
|
+
await this.ensureConfigurationReady();
|
|
32
84
|
const succeeded = [];
|
|
33
85
|
const failed = [];
|
|
34
86
|
const depGraph = this.buildModuleDependencyGraph();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const moduleStartTime = Date.now();
|
|
47
|
-
try {
|
|
48
|
-
if (module.onInitialize) {
|
|
49
|
-
const context = this.createModuleContext(module);
|
|
50
|
-
await module.onInitialize(context);
|
|
87
|
+
// Modules within a depth group have no dependency on one another, so
|
|
88
|
+
// they may be initialized together when the caller opts in.
|
|
89
|
+
const groups = this.options.parallelInitialization
|
|
90
|
+
? depGraph.parallelGroups
|
|
91
|
+
: depGraph.order.map((moduleId) => [moduleId]);
|
|
92
|
+
for (const group of groups) {
|
|
93
|
+
const results = await Promise.all(group.map((moduleId) => this.initializeModule(moduleId)));
|
|
94
|
+
for (const result of results) {
|
|
95
|
+
if (result.failure) {
|
|
96
|
+
failed.push(result.failure);
|
|
51
97
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
durationMs: Date.now() - moduleStartTime,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
const failure = {
|
|
60
|
-
moduleId,
|
|
61
|
-
phase: "initialize",
|
|
62
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
63
|
-
durationMs: Date.now() - moduleStartTime,
|
|
64
|
-
};
|
|
65
|
-
failed.push(failure);
|
|
66
|
-
this.logger.error(`Module "${moduleId}" failed during initialization.`, { error: failure.error });
|
|
67
|
-
if (!this.options.continueOnFailure) {
|
|
68
|
-
break;
|
|
98
|
+
else {
|
|
99
|
+
succeeded.push(result.moduleId);
|
|
100
|
+
this.initializedModules.push(result.moduleId);
|
|
69
101
|
}
|
|
70
102
|
}
|
|
103
|
+
if (failed.length > 0 && !this.options.continueOnFailure) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
71
106
|
}
|
|
72
107
|
return Object.freeze({
|
|
73
108
|
phase: "initialize",
|
|
@@ -76,11 +111,58 @@ export class LifecycleManager {
|
|
|
76
111
|
durationMs: Date.now() - startTime,
|
|
77
112
|
});
|
|
78
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Initializes a single module, converting a throw into a failure.
|
|
116
|
+
*/
|
|
117
|
+
async initializeModule(moduleId) {
|
|
118
|
+
const module = this.modules.get(moduleId);
|
|
119
|
+
if (!module) {
|
|
120
|
+
return {
|
|
121
|
+
moduleId,
|
|
122
|
+
failure: {
|
|
123
|
+
moduleId,
|
|
124
|
+
phase: "initialize",
|
|
125
|
+
error: new RuntimeDependencyError(moduleId, "unknown"),
|
|
126
|
+
durationMs: 0,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
this.emitModuleEvent("runtime.module.initializing", moduleId, "initializing");
|
|
132
|
+
try {
|
|
133
|
+
if (module.onInitialize) {
|
|
134
|
+
await module.onInitialize(this.createModuleContext(module));
|
|
135
|
+
}
|
|
136
|
+
const durationMs = Date.now() - startedAt;
|
|
137
|
+
this.logger.debug(`Module "${moduleId}" initialized.`, { durationMs });
|
|
138
|
+
this.emitModuleEvent("runtime.module.initialized", moduleId, "initialized", {
|
|
139
|
+
durationMs,
|
|
140
|
+
});
|
|
141
|
+
return { moduleId };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const failure = {
|
|
145
|
+
moduleId,
|
|
146
|
+
phase: "initialize",
|
|
147
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
148
|
+
durationMs: Date.now() - startedAt,
|
|
149
|
+
};
|
|
150
|
+
this.logger.error(`Module "${moduleId}" failed during initialization.`, {
|
|
151
|
+
error: failure.error,
|
|
152
|
+
});
|
|
153
|
+
this.emitModuleEvent("runtime.module.failed", moduleId, "failed", {
|
|
154
|
+
durationMs: failure.durationMs,
|
|
155
|
+
error: failure.error,
|
|
156
|
+
});
|
|
157
|
+
return { moduleId, failure };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
79
160
|
/**
|
|
80
161
|
* Starts all modules in dependency order.
|
|
81
162
|
*/
|
|
82
163
|
async start() {
|
|
83
164
|
const startTime = Date.now();
|
|
165
|
+
this.startedModules = [];
|
|
84
166
|
const succeeded = [];
|
|
85
167
|
const failed = [];
|
|
86
168
|
for (const moduleId of this.initializedModules) {
|
|
@@ -88,6 +170,7 @@ export class LifecycleManager {
|
|
|
88
170
|
if (!module)
|
|
89
171
|
continue;
|
|
90
172
|
const moduleStartTime = Date.now();
|
|
173
|
+
this.emitModuleEvent("runtime.module.starting", moduleId, "starting");
|
|
91
174
|
try {
|
|
92
175
|
if (module.onReady) {
|
|
93
176
|
const context = this.createModuleContext(module);
|
|
@@ -95,8 +178,10 @@ export class LifecycleManager {
|
|
|
95
178
|
}
|
|
96
179
|
succeeded.push(moduleId);
|
|
97
180
|
this.startedModules.push(moduleId);
|
|
98
|
-
|
|
99
|
-
|
|
181
|
+
const durationMs = Date.now() - moduleStartTime;
|
|
182
|
+
this.logger.debug(`Module "${moduleId}" started.`, { durationMs });
|
|
183
|
+
this.emitModuleEvent("runtime.module.started", moduleId, "started", {
|
|
184
|
+
durationMs,
|
|
100
185
|
});
|
|
101
186
|
}
|
|
102
187
|
catch (error) {
|
|
@@ -110,6 +195,10 @@ export class LifecycleManager {
|
|
|
110
195
|
this.logger.error(`Module "${moduleId}" failed during startup.`, {
|
|
111
196
|
error: failure.error,
|
|
112
197
|
});
|
|
198
|
+
this.emitModuleEvent("runtime.module.failed", moduleId, "failed", {
|
|
199
|
+
durationMs: failure.durationMs,
|
|
200
|
+
error: failure.error,
|
|
201
|
+
});
|
|
113
202
|
if (!this.options.continueOnFailure) {
|
|
114
203
|
break;
|
|
115
204
|
}
|
|
@@ -135,14 +224,17 @@ export class LifecycleManager {
|
|
|
135
224
|
if (!module)
|
|
136
225
|
continue;
|
|
137
226
|
const moduleStartTime = Date.now();
|
|
227
|
+
this.emitModuleEvent("runtime.module.stopping", moduleId, "stopping");
|
|
138
228
|
try {
|
|
139
229
|
if (module.onShutdown) {
|
|
140
230
|
const context = this.createModuleContext(module);
|
|
141
231
|
await module.onShutdown(context);
|
|
142
232
|
}
|
|
143
233
|
succeeded.push(moduleId);
|
|
144
|
-
|
|
145
|
-
|
|
234
|
+
const durationMs = Date.now() - moduleStartTime;
|
|
235
|
+
this.logger.debug(`Module "${moduleId}" stopped.`, { durationMs });
|
|
236
|
+
this.emitModuleEvent("runtime.module.stopped", moduleId, "stopped", {
|
|
237
|
+
durationMs,
|
|
146
238
|
});
|
|
147
239
|
}
|
|
148
240
|
catch (error) {
|
|
@@ -156,6 +248,10 @@ export class LifecycleManager {
|
|
|
156
248
|
this.logger.error(`Module "${moduleId}" failed during shutdown.`, {
|
|
157
249
|
error: failure.error,
|
|
158
250
|
});
|
|
251
|
+
this.emitModuleEvent("runtime.module.failed", moduleId, "failed", {
|
|
252
|
+
durationMs: failure.durationMs,
|
|
253
|
+
error: failure.error,
|
|
254
|
+
});
|
|
159
255
|
}
|
|
160
256
|
}
|
|
161
257
|
return Object.freeze({
|
|
@@ -203,6 +299,11 @@ export class LifecycleManager {
|
|
|
203
299
|
});
|
|
204
300
|
}
|
|
205
301
|
}
|
|
302
|
+
// The runtime no longer owns these modules; a subsequent start
|
|
303
|
+
// rebuilds the lists from scratch.
|
|
304
|
+
this.startedModules = [];
|
|
305
|
+
this.initializedModules = [];
|
|
306
|
+
this.contexts.clear();
|
|
206
307
|
return Object.freeze({
|
|
207
308
|
phase: "destroy",
|
|
208
309
|
succeeded: Object.freeze(succeeded),
|
|
@@ -211,51 +312,127 @@ export class LifecycleManager {
|
|
|
211
312
|
});
|
|
212
313
|
}
|
|
213
314
|
/**
|
|
214
|
-
* Rolls back
|
|
315
|
+
* Rolls back a failed startup.
|
|
316
|
+
*
|
|
317
|
+
* Started modules are shut down, and every module that reached
|
|
318
|
+
* `onInitialize` is then destroyed — including those that never
|
|
319
|
+
* started because an earlier module failed. Rolling back only the
|
|
320
|
+
* started ones leaves everything they had already acquired behind with
|
|
321
|
+
* no route to release it.
|
|
215
322
|
*/
|
|
216
323
|
async rollback() {
|
|
217
|
-
const
|
|
218
|
-
for (const moduleId of
|
|
324
|
+
const failures = [];
|
|
325
|
+
for (const moduleId of [...this.startedModules].reverse()) {
|
|
219
326
|
const module = this.modules.get(moduleId);
|
|
220
327
|
if (!module?.onShutdown)
|
|
221
328
|
continue;
|
|
329
|
+
const startedAt = Date.now();
|
|
222
330
|
try {
|
|
223
|
-
|
|
224
|
-
await module.onShutdown(context);
|
|
331
|
+
await module.onShutdown(this.createModuleContext(module));
|
|
225
332
|
this.logger.debug(`Module "${moduleId}" rolled back.`);
|
|
226
333
|
}
|
|
227
334
|
catch (error) {
|
|
335
|
+
failures.push({
|
|
336
|
+
moduleId,
|
|
337
|
+
phase: "stop",
|
|
338
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
339
|
+
durationMs: Date.now() - startedAt,
|
|
340
|
+
});
|
|
228
341
|
this.logger.error(`Module "${moduleId}" rollback failed.`, {
|
|
229
342
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
230
343
|
});
|
|
231
344
|
}
|
|
232
345
|
}
|
|
346
|
+
for (const moduleId of [...this.initializedModules].reverse()) {
|
|
347
|
+
const module = this.modules.get(moduleId);
|
|
348
|
+
if (!module?.onDestroy)
|
|
349
|
+
continue;
|
|
350
|
+
const startedAt = Date.now();
|
|
351
|
+
try {
|
|
352
|
+
await module.onDestroy(this.createModuleContext(module));
|
|
353
|
+
this.logger.debug(`Module "${moduleId}" destroyed during rollback.`);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
failures.push({
|
|
357
|
+
moduleId,
|
|
358
|
+
phase: "destroy",
|
|
359
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
360
|
+
durationMs: Date.now() - startedAt,
|
|
361
|
+
});
|
|
362
|
+
this.logger.error(`Module "${moduleId}" destruction failed.`, {
|
|
363
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// A rolled-back runtime owns nothing; clear the bookkeeping so a
|
|
368
|
+
// later shutdown does not stop modules a second time.
|
|
369
|
+
this.startedModules = [];
|
|
370
|
+
this.initializedModules = [];
|
|
371
|
+
this.contexts.clear();
|
|
372
|
+
return failures;
|
|
233
373
|
}
|
|
234
374
|
/**
|
|
235
375
|
* Creates a module context for lifecycle hooks.
|
|
236
376
|
*/
|
|
237
377
|
createModuleContext(module) {
|
|
238
|
-
|
|
378
|
+
const existing = this.contexts.get(module.id);
|
|
379
|
+
if (existing !== undefined) {
|
|
380
|
+
return existing;
|
|
381
|
+
}
|
|
382
|
+
const configuration = this.configuration;
|
|
383
|
+
const application = this.application;
|
|
384
|
+
const logger = this.logger;
|
|
385
|
+
const contexts = this.contexts;
|
|
386
|
+
const modules = this.modules;
|
|
387
|
+
const context = {
|
|
239
388
|
id: module.id,
|
|
240
389
|
name: module.name,
|
|
241
390
|
version: module.version,
|
|
242
391
|
options: module.options ?? {},
|
|
243
392
|
scope: module.scope,
|
|
244
|
-
application
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
393
|
+
// `application` is a getter so that a runtime started without an
|
|
394
|
+
// application context fails loudly at the point of use, rather than
|
|
395
|
+
// handing modules an empty object that lies about its type.
|
|
396
|
+
get application() {
|
|
397
|
+
if (application === undefined) {
|
|
398
|
+
throw new RuntimeStateError(`Module "${module.id}" accessed "context.application", but no ` +
|
|
399
|
+
`ApplicationContext was supplied to the runtime.`);
|
|
400
|
+
}
|
|
401
|
+
return application;
|
|
251
402
|
},
|
|
252
|
-
|
|
253
|
-
|
|
403
|
+
configuration,
|
|
404
|
+
logger,
|
|
405
|
+
getConfiguration: () => configuration.getConfiguration(),
|
|
406
|
+
getConfig: (path) => configuration.get(path),
|
|
407
|
+
requireConfig: (path) => configuration.require(path),
|
|
408
|
+
// Only a module's declared dependencies are reachable. Handing
|
|
409
|
+
// every module every other module's configuration and application
|
|
410
|
+
// context makes the declared dependency graph advisory.
|
|
411
|
+
getModuleContext: (moduleId) => {
|
|
412
|
+
const allowed = moduleId === module.id ||
|
|
413
|
+
(module.dependencies ?? []).includes(moduleId);
|
|
414
|
+
return allowed ? contexts.get(moduleId) : undefined;
|
|
415
|
+
},
|
|
416
|
+
hasModule: (moduleId) => modules.has(moduleId),
|
|
254
417
|
};
|
|
418
|
+
this.contexts.set(module.id, context);
|
|
419
|
+
return context;
|
|
255
420
|
}
|
|
256
421
|
/**
|
|
257
422
|
* Builds a dependency graph from registered modules.
|
|
258
423
|
*/
|
|
424
|
+
/**
|
|
425
|
+
* Modules that reached `onInitialize`, in initialization order.
|
|
426
|
+
*/
|
|
427
|
+
getInitializedModules() {
|
|
428
|
+
return Object.freeze([...this.initializedModules]);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Modules that reached `onReady`, in start order.
|
|
432
|
+
*/
|
|
433
|
+
getStartedModules() {
|
|
434
|
+
return Object.freeze([...this.startedModules]);
|
|
435
|
+
}
|
|
259
436
|
buildModuleDependencyGraph() {
|
|
260
437
|
const moduleDeps = new Map();
|
|
261
438
|
for (const [id, module] of this.modules) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Module } from "@zudojs/core";
|
|
1
|
+
import type { ConfigurationManager, Module, ModuleContext } from "@zudojs/core";
|
|
2
|
+
import type { RuntimeModuleEventPayload, RuntimeModuleEventType } from "../runtimeEvents/runtimeEvents.type.js";
|
|
2
3
|
/**
|
|
3
4
|
* Lifecycle hook phases for modules.
|
|
4
5
|
*/
|
|
@@ -40,12 +41,58 @@ export interface ManagedModule {
|
|
|
40
41
|
readonly depth: number;
|
|
41
42
|
readonly dependencies: readonly string[];
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Framework services made available to modules through their
|
|
46
|
+
* {@link ModuleContext}.
|
|
47
|
+
*
|
|
48
|
+
* Both are optional so a runtime can be started without a configuration
|
|
49
|
+
* layer, but a module that reaches for a service the host did not supply
|
|
50
|
+
* gets a clear error rather than a silently empty object.
|
|
51
|
+
*/
|
|
52
|
+
export interface ModuleContextServices {
|
|
53
|
+
/**
|
|
54
|
+
* Configuration manager backing `getConfiguration`, `getConfig` and
|
|
55
|
+
* `requireConfig`. Defaults to an empty, uninitialized manager.
|
|
56
|
+
*/
|
|
57
|
+
readonly configuration?: ConfigurationManager;
|
|
58
|
+
/**
|
|
59
|
+
* Application context exposed as `context.application`.
|
|
60
|
+
*
|
|
61
|
+
* `ApplicationContext` is not part of `@zudojs/core`'s published entry
|
|
62
|
+
* points, so the runtime cannot construct one itself; a host that has an
|
|
63
|
+
* application context passes it in here.
|
|
64
|
+
*/
|
|
65
|
+
readonly application?: ModuleContext["application"];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Receives a per-module lifecycle event.
|
|
69
|
+
*/
|
|
70
|
+
export type ModuleEventListener = (type: RuntimeModuleEventType, payload: RuntimeModuleEventPayload) => void;
|
|
43
71
|
/**
|
|
44
72
|
* Options for lifecycle management.
|
|
45
73
|
*/
|
|
46
74
|
export interface LifecycleManagerOptions {
|
|
47
75
|
readonly shutdownTimeout?: number;
|
|
48
76
|
readonly continueOnFailure?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Receives one event per module per lifecycle phase.
|
|
79
|
+
*
|
|
80
|
+
* The lifecycle is the only layer that knows which module is running,
|
|
81
|
+
* so it is the only layer that can populate a
|
|
82
|
+
* {@link RuntimeModuleEventPayload}. Without this the runtime published
|
|
83
|
+
* two phase-wide events with no module on them, and every
|
|
84
|
+
* `runtime.module.*` entry in `RuntimeEventMap` named a payload nothing
|
|
85
|
+
* produced.
|
|
86
|
+
*/
|
|
87
|
+
readonly onModuleEvent?: ModuleEventListener;
|
|
88
|
+
/** Runtime identifier stamped onto emitted module events. */
|
|
89
|
+
readonly runtimeId?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Whether modules at the same dependency depth are initialized
|
|
92
|
+
* concurrently. Defaults to `false`. Modules within a depth group do
|
|
93
|
+
* not depend on one another, but enabling this surfaces any ordering
|
|
94
|
+
* a module assumed without declaring.
|
|
95
|
+
*/
|
|
49
96
|
readonly parallelInitialization?: boolean;
|
|
50
97
|
}
|
|
51
98
|
//# sourceMappingURL=lifecycle.type.d.ts.map
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Runtime readiness tracking.
|
|
3
3
|
*/
|
|
4
4
|
export { ReadinessTracker } from "./readiness.core.js";
|
|
5
|
-
export type { ReadinessState, ReadinessCheck, ReadinessTrackerState, ReadinessOptions, } from "./readiness.type.js";
|
|
5
|
+
export type { ReadinessState, ReadinessCheck, ReadinessCheckFn, ReadinessTrackerState, ReadinessOptions, } from "./readiness.type.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,22 +1,54 @@
|
|
|
1
|
-
import type { ReadinessState, ReadinessTrackerState, ReadinessOptions } from "./readiness.type.js";
|
|
1
|
+
import type { ReadinessState, ReadinessCheckFn, ReadinessTrackerState, ReadinessOptions } from "./readiness.type.js";
|
|
2
2
|
/**
|
|
3
3
|
* Tracks runtime readiness state.
|
|
4
4
|
*/
|
|
5
5
|
export declare class ReadinessTracker {
|
|
6
6
|
private state;
|
|
7
7
|
private readonly checks;
|
|
8
|
+
private readonly checkFns;
|
|
8
9
|
private readonly autoMarkReady;
|
|
10
|
+
private readonly checkTimeout;
|
|
9
11
|
private ready;
|
|
10
12
|
private reason?;
|
|
13
|
+
private running;
|
|
11
14
|
constructor(options?: ReadinessOptions);
|
|
12
15
|
/**
|
|
13
16
|
* Registers a readiness check.
|
|
17
|
+
*
|
|
18
|
+
* The check function is retained so it can be re-evaluated later by
|
|
19
|
+
* `updateCheck(name)` or `runChecks()`. Registration does not run the
|
|
20
|
+
* check: it starts out not-ready until it is first evaluated.
|
|
14
21
|
*/
|
|
15
|
-
registerCheck(name: string, check:
|
|
22
|
+
registerCheck(name: string, check: ReadinessCheckFn): void;
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
24
|
+
* Removes a readiness check.
|
|
25
|
+
*
|
|
26
|
+
* Returns whether a check with that name was registered.
|
|
18
27
|
*/
|
|
19
|
-
|
|
28
|
+
removeCheck(name: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Evaluates a readiness check and records its result.
|
|
31
|
+
*
|
|
32
|
+
* Re-runs the function registered under `name` unless a replacement is
|
|
33
|
+
* supplied, in which case the replacement is stored and used from then on.
|
|
34
|
+
*/
|
|
35
|
+
updateCheck(name: string, check?: ReadinessCheckFn): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Re-evaluates every registered check.
|
|
38
|
+
*/
|
|
39
|
+
runChecks(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether any readiness check is registered.
|
|
42
|
+
*/
|
|
43
|
+
hasChecks(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Runs a single check and records its result and duration.
|
|
46
|
+
*/
|
|
47
|
+
private evaluateCheck;
|
|
48
|
+
/**
|
|
49
|
+
* Runs a check under a timeout, clearing the timer either way.
|
|
50
|
+
*/
|
|
51
|
+
private withCheckTimeout;
|
|
20
52
|
/**
|
|
21
53
|
* Marks the runtime as ready.
|
|
22
54
|
*/
|