@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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -6
  3. package/dist/dependencyGraph/dependencyGraph.core.d.ts +11 -5
  4. package/dist/dependencyGraph/dependencyGraph.core.js +13 -12
  5. package/dist/dependencyGraph/dependencyGraph.type.d.ts +0 -7
  6. package/dist/dependencyGraph/index.d.ts +2 -2
  7. package/dist/dependencyGraph/index.js +1 -1
  8. package/dist/health/health.core.d.ts +12 -0
  9. package/dist/health/health.core.js +55 -0
  10. package/dist/health/index.d.ts +5 -0
  11. package/dist/health/index.js +5 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +4 -1
  14. package/dist/lifecycle/index.d.ts +1 -1
  15. package/dist/lifecycle/lifecycle.core.d.ts +41 -10
  16. package/dist/lifecycle/lifecycle.core.js +238 -61
  17. package/dist/lifecycle/lifecycle.type.d.ts +48 -1
  18. package/dist/readiness/index.d.ts +1 -1
  19. package/dist/readiness/readiness.core.d.ts +36 -4
  20. package/dist/readiness/readiness.core.js +122 -6
  21. package/dist/readiness/readiness.type.d.ts +15 -1
  22. package/dist/registry/registry.core.d.ts +14 -1
  23. package/dist/registry/registry.core.js +41 -2
  24. package/dist/runtime/runtime.core.d.ts +75 -5
  25. package/dist/runtime/runtime.core.js +210 -43
  26. package/dist/runtimeContext/index.d.ts +2 -2
  27. package/dist/runtimeContext/index.js +1 -1
  28. package/dist/runtimeContext/runtimeContext.core.d.ts +13 -4
  29. package/dist/runtimeContext/runtimeContext.core.js +22 -11
  30. package/dist/runtimeContext/runtimeContext.type.d.ts +12 -13
  31. package/dist/runtimeError/runtimeError.base.js +1 -1
  32. package/dist/runtimeEvents/index.d.ts +2 -2
  33. package/dist/runtimeEvents/index.js +1 -1
  34. package/dist/runtimeEvents/runtimeEvents.core.d.ts +15 -1
  35. package/dist/runtimeEvents/runtimeEvents.core.js +26 -0
  36. package/dist/runtimeEvents/runtimeEvents.type.d.ts +19 -4
  37. package/dist/runtimeOptions/runtimeOptions.core.d.ts +5 -0
  38. package/dist/runtimeOptions/runtimeOptions.core.js +14 -1
  39. package/dist/runtimeOptions/runtimeOptions.type.d.ts +29 -1
  40. package/dist/runtimeOptions/runtimeOptions.type.js +6 -0
  41. package/dist/runtimeState/index.d.ts +1 -1
  42. package/dist/runtimeState/runtimeState.core.d.ts +4 -0
  43. package/dist/runtimeState/runtimeState.core.js +17 -4
  44. package/dist/runtimeState/runtimeState.type.d.ts +14 -17
  45. package/dist/shutdown/shutdown.core.d.ts +20 -1
  46. package/dist/shutdown/shutdown.core.js +55 -29
  47. package/dist/signalHandler/signalHandler.core.d.ts +53 -13
  48. package/dist/signalHandler/signalHandler.core.js +97 -27
  49. package/dist/startup/startup.core.d.ts +3 -2
  50. package/dist/startup/startup.core.js +52 -46
  51. package/dist/testRuntime/index.d.ts +6 -1
  52. package/dist/testRuntime/index.js +5 -1
  53. package/dist/testRuntime/testRuntime.core.d.ts +21 -2
  54. package/dist/testRuntime/testRuntime.core.js +23 -6
  55. package/package.json +33 -22
  56. package/dist/.tsbuildinfo +0 -1
  57. package/dist/dependencyGraph/dependencyGraph.core.d.ts.map +0 -1
  58. package/dist/dependencyGraph/dependencyGraph.core.js.map +0 -1
  59. package/dist/dependencyGraph/dependencyGraph.type.d.ts.map +0 -1
  60. package/dist/dependencyGraph/dependencyGraph.type.js.map +0 -1
  61. package/dist/dependencyGraph/index.d.ts.map +0 -1
  62. package/dist/dependencyGraph/index.js.map +0 -1
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js.map +0 -1
  65. package/dist/lifecycle/index.d.ts.map +0 -1
  66. package/dist/lifecycle/index.js.map +0 -1
  67. package/dist/lifecycle/lifecycle.core.d.ts.map +0 -1
  68. package/dist/lifecycle/lifecycle.core.js.map +0 -1
  69. package/dist/lifecycle/lifecycle.type.d.ts.map +0 -1
  70. package/dist/lifecycle/lifecycle.type.js.map +0 -1
  71. package/dist/readiness/index.d.ts.map +0 -1
  72. package/dist/readiness/index.js.map +0 -1
  73. package/dist/readiness/readiness.core.d.ts.map +0 -1
  74. package/dist/readiness/readiness.core.js.map +0 -1
  75. package/dist/readiness/readiness.type.d.ts.map +0 -1
  76. package/dist/readiness/readiness.type.js.map +0 -1
  77. package/dist/registry/index.d.ts.map +0 -1
  78. package/dist/registry/index.js.map +0 -1
  79. package/dist/registry/registry.core.d.ts.map +0 -1
  80. package/dist/registry/registry.core.js.map +0 -1
  81. package/dist/runtime/runtime.core.d.ts.map +0 -1
  82. package/dist/runtime/runtime.core.js.map +0 -1
  83. package/dist/runtimeContext/index.d.ts.map +0 -1
  84. package/dist/runtimeContext/index.js.map +0 -1
  85. package/dist/runtimeContext/runtimeContext.core.d.ts.map +0 -1
  86. package/dist/runtimeContext/runtimeContext.core.js.map +0 -1
  87. package/dist/runtimeContext/runtimeContext.factory.d.ts.map +0 -1
  88. package/dist/runtimeContext/runtimeContext.factory.js.map +0 -1
  89. package/dist/runtimeContext/runtimeContext.type.d.ts.map +0 -1
  90. package/dist/runtimeContext/runtimeContext.type.js.map +0 -1
  91. package/dist/runtimeError/index.d.ts.map +0 -1
  92. package/dist/runtimeError/index.js.map +0 -1
  93. package/dist/runtimeError/runtimeError.base.d.ts.map +0 -1
  94. package/dist/runtimeError/runtimeError.base.js.map +0 -1
  95. package/dist/runtimeEvents/index.d.ts.map +0 -1
  96. package/dist/runtimeEvents/index.js.map +0 -1
  97. package/dist/runtimeEvents/runtimeEvents.core.d.ts.map +0 -1
  98. package/dist/runtimeEvents/runtimeEvents.core.js.map +0 -1
  99. package/dist/runtimeEvents/runtimeEvents.type.d.ts.map +0 -1
  100. package/dist/runtimeEvents/runtimeEvents.type.js.map +0 -1
  101. package/dist/runtimeOptions/index.d.ts.map +0 -1
  102. package/dist/runtimeOptions/index.js.map +0 -1
  103. package/dist/runtimeOptions/runtimeOptions.core.d.ts.map +0 -1
  104. package/dist/runtimeOptions/runtimeOptions.core.js.map +0 -1
  105. package/dist/runtimeOptions/runtimeOptions.type.d.ts.map +0 -1
  106. package/dist/runtimeOptions/runtimeOptions.type.js.map +0 -1
  107. package/dist/runtimeState/index.d.ts.map +0 -1
  108. package/dist/runtimeState/index.js.map +0 -1
  109. package/dist/runtimeState/runtimeState.core.d.ts.map +0 -1
  110. package/dist/runtimeState/runtimeState.core.js.map +0 -1
  111. package/dist/runtimeState/runtimeState.type.d.ts.map +0 -1
  112. package/dist/runtimeState/runtimeState.type.js.map +0 -1
  113. package/dist/shutdown/index.d.ts.map +0 -1
  114. package/dist/shutdown/index.js.map +0 -1
  115. package/dist/shutdown/shutdown.core.d.ts.map +0 -1
  116. package/dist/shutdown/shutdown.core.js.map +0 -1
  117. package/dist/signalHandler/index.d.ts.map +0 -1
  118. package/dist/signalHandler/index.js.map +0 -1
  119. package/dist/signalHandler/signalHandler.core.d.ts.map +0 -1
  120. package/dist/signalHandler/signalHandler.core.js.map +0 -1
  121. package/dist/startup/index.d.ts.map +0 -1
  122. package/dist/startup/index.js.map +0 -1
  123. package/dist/startup/startup.core.d.ts.map +0 -1
  124. package/dist/startup/startup.core.js.map +0 -1
  125. package/dist/testRuntime/index.d.ts.map +0 -1
  126. package/dist/testRuntime/index.js.map +0 -1
  127. package/dist/testRuntime/testRuntime.core.d.ts.map +0 -1
  128. package/dist/testRuntime/testRuntime.core.js.map +0 -1
@@ -1,27 +1,26 @@
1
- import { canTransition, isRunning, createStatus, } from "../runtimeState/index.js";
1
+ import { assertTransition, canStart, canStop, hasFailed, isRunning, } from "../runtimeState/index.js";
2
2
  import { createRuntimeOptions } from "../runtimeOptions/index.js";
3
- import { createRuntimeContext } from "../runtimeContext/index.js";
4
- import { createRuntimeId } from "../runtimeContext/index.js";
3
+ import { createRuntimeContext, withRuntimeContextState, } from "../runtimeContext/index.js";
5
4
  import { LifecycleManager } from "../lifecycle/index.js";
6
5
  import { executeStartup, rollbackStartup } from "../startup/index.js";
7
6
  import { executeShutdown } from "../shutdown/index.js";
8
7
  import { SignalHandler } from "../signalHandler/index.js";
9
8
  import { ReadinessTracker } from "../readiness/index.js";
10
- import { createRuntimeEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, } from "../runtimeEvents/index.js";
9
+ import { computeRuntimeHealth } from "../health/index.js";
10
+ import { createRuntimeEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, publishRuntimeEvent, } from "../runtimeEvents/index.js";
11
11
  import { createEvent } from "@zudojs/events";
12
- import { RuntimeStartError, RuntimeStopError, RuntimeStateError, RuntimeTimeoutError, RuntimeRollbackError, toRuntimeError, } from "../runtimeError/index.js";
12
+ import { RuntimeStateError, toRuntimeError } from "../runtimeError/index.js";
13
13
  /**
14
14
  * Default runtime implementation.
15
15
  */
16
16
  export class DefaultRuntime {
17
17
  _state = "created";
18
- _status;
19
- _context;
20
- _ready = false;
18
+ _contextBase;
21
19
  _startedAt;
22
20
  _stoppedAt;
23
21
  _failedAt;
24
22
  _error;
23
+ _shutdownFailures = [];
25
24
  options;
26
25
  modules;
27
26
  logger;
@@ -34,7 +33,7 @@ export class DefaultRuntime {
34
33
  this.options = createRuntimeOptions(options);
35
34
  this.modules = dependencies.modules;
36
35
  this.logger = dependencies.logger;
37
- this._context = createRuntimeContext({
36
+ this._contextBase = createRuntimeContext({
38
37
  runtimeId: this.options.runtimeId,
39
38
  environment: this.options.environment,
40
39
  applicationName: this.options.applicationName,
@@ -42,11 +41,26 @@ export class DefaultRuntime {
42
41
  logger: dependencies.logger,
43
42
  container: dependencies.container,
44
43
  eventBus: dependencies.eventBus,
44
+ metadata: this.options.metadata,
45
45
  });
46
- this._status = createStatus("created");
47
- this.lifecycle = new LifecycleManager(this.modules, this.logger, dependencies.container, this.options.runtimeId, this.options.environment, {
46
+ this.lifecycle = new LifecycleManager(this.modules, this.logger, {
48
47
  shutdownTimeout: this.options.shutdownTimeout,
48
+ // Always false during startup: rollback depends on stopping at
49
+ // the first failure rather than pressing on into modules whose
50
+ // dependencies never came up.
49
51
  continueOnFailure: false,
52
+ parallelInitialization: this.options.parallelInitialization,
53
+ runtimeId: this.options.runtimeId,
54
+ onModuleEvent: (type, payload) => {
55
+ this.emitEvent(type, payload);
56
+ },
57
+ }, {
58
+ ...(dependencies.configuration !== undefined && {
59
+ configuration: dependencies.configuration,
60
+ }),
61
+ ...(dependencies.application !== undefined && {
62
+ application: dependencies.application,
63
+ }),
50
64
  });
51
65
  this.signalHandler = new SignalHandler(this.logger, {
52
66
  handleSignals: this.options.handleSignals,
@@ -54,6 +68,7 @@ export class DefaultRuntime {
54
68
  });
55
69
  this.readinessTracker = new ReadinessTracker({
56
70
  autoMarkReady: this.options.trackReadiness,
71
+ checkTimeout: this.options.readinessCheckTimeout,
57
72
  });
58
73
  }
59
74
  /**
@@ -66,19 +81,88 @@ export class DefaultRuntime {
66
81
  * Current runtime status.
67
82
  */
68
83
  get status() {
69
- return this._status;
84
+ return Object.freeze({
85
+ state: this._state,
86
+ ready: this.ready,
87
+ running: isRunning(this._state),
88
+ startedAt: this._startedAt,
89
+ stoppedAt: this._stoppedAt,
90
+ failedAt: this._failedAt,
91
+ error: this._error,
92
+ shutdownFailures: this._shutdownFailures,
93
+ });
70
94
  }
71
95
  /**
72
96
  * Runtime context.
73
97
  */
74
98
  get context() {
75
- return this._context;
99
+ return withRuntimeContextState(this._contextBase, {
100
+ status: this.status,
101
+ health: this.health,
102
+ ready: this.ready,
103
+ ...(this._startedAt !== undefined && { startedAt: this._startedAt }),
104
+ ...(this._stoppedAt !== undefined && { stoppedAt: this._stoppedAt }),
105
+ ...(this._failedAt !== undefined && { failedAt: this._failedAt }),
106
+ ...(this._error !== undefined && { error: this._error }),
107
+ });
108
+ }
109
+ /**
110
+ * Current runtime health, derived from the lifecycle state and the
111
+ * registered readiness checks.
112
+ */
113
+ get health() {
114
+ if (!this.options.trackHealth) {
115
+ return Object.freeze({
116
+ state: "unknown",
117
+ checks: Object.freeze([]),
118
+ timestamp: new Date(),
119
+ });
120
+ }
121
+ return computeRuntimeHealth(this._state, this.readinessTracker.getState());
122
+ }
123
+ /**
124
+ * Current readiness state, including per-check results.
125
+ */
126
+ get readiness() {
127
+ return this.readinessTracker.getState();
128
+ }
129
+ /**
130
+ * Registers a readiness check.
131
+ */
132
+ registerReadinessCheck(name, check) {
133
+ const previousHealth = this.health.state;
134
+ this.readinessTracker.registerCheck(name, check);
135
+ this.emitHealthChange(previousHealth);
136
+ }
137
+ /**
138
+ * Removes a readiness check.
139
+ */
140
+ removeReadinessCheck(name) {
141
+ const previousHealth = this.health.state;
142
+ const removed = this.readinessTracker.removeCheck(name);
143
+ if (removed) {
144
+ this.emitHealthChange(previousHealth);
145
+ }
146
+ return removed;
147
+ }
148
+ /**
149
+ * Re-evaluates every registered readiness check.
150
+ */
151
+ async runReadinessChecks() {
152
+ const previousHealth = this.health.state;
153
+ const previouslyReady = this.ready;
154
+ await this.readinessTracker.runChecks();
155
+ this.emitHealthChange(previousHealth);
156
+ if (this.options.emitEvents && this.ready !== previouslyReady) {
157
+ const state = this.readinessTracker.getState();
158
+ this.emitEvent("runtime.readiness.changed", createReadinessEventPayload(this.options.runtimeId, this._state, state.ready, state.reason));
159
+ }
76
160
  }
77
161
  /**
78
162
  * Whether the runtime is ready.
79
163
  */
80
164
  get ready() {
81
- return this._ready;
165
+ return this.readinessTracker.isReady();
82
166
  }
83
167
  /**
84
168
  * Starts the runtime.
@@ -90,8 +174,11 @@ export class DefaultRuntime {
90
174
  if (this.startPromise) {
91
175
  return this.startPromise;
92
176
  }
93
- if (!canTransition(this._state, "initializing")) {
94
- throw new RuntimeStateError(`Cannot start runtime from state "${this._state}".`);
177
+ if (!canStart(this._state)) {
178
+ throw new RuntimeStateError(`Cannot start a runtime in state "${this._state}"; start is only valid from "created". ` +
179
+ (hasFailed(this._state)
180
+ ? "This runtime failed to start; call stop() to release it and create a new one."
181
+ : "Create a new runtime instead of restarting this one."));
95
182
  }
96
183
  this.startPromise = this.performStart();
97
184
  try {
@@ -104,6 +191,14 @@ export class DefaultRuntime {
104
191
  /**
105
192
  * Stops the runtime.
106
193
  */
194
+ /**
195
+ * Stops the runtime.
196
+ *
197
+ * A failed runtime is stoppable: startup rollback only reaches modules
198
+ * that were started, so this is the operator's route to releasing
199
+ * everything else. It is also idempotent — stopping an already-stopped
200
+ * runtime is a no-op rather than an error.
201
+ */
107
202
  async stop() {
108
203
  if (this._state === "stopped") {
109
204
  return;
@@ -111,13 +206,25 @@ export class DefaultRuntime {
111
206
  if (this.stopPromise) {
112
207
  return this.stopPromise;
113
208
  }
209
+ if (this.startPromise) {
210
+ // A stop requested while startup is in flight — typically a SIGTERM
211
+ // arriving while modules are still coming up — waits for startup to
212
+ // settle and then shuts down whatever it produced. Previously this
213
+ // threw `RuntimeStateError` ("cannot stop a runtime in state
214
+ // initializing"), so the signal handler logged "Shutdown failed" and
215
+ // the runtime carried on to `running` as if nothing had happened.
216
+ // Startup is bounded by `startupTimeout`, so this wait is too.
217
+ await this.startPromise.catch(() => undefined);
218
+ return this.stop();
219
+ }
114
220
  if (this._state === "created") {
115
221
  this._state = "stopped";
116
222
  this._stoppedAt = new Date();
223
+ this.signalHandler.unregister();
117
224
  return;
118
225
  }
119
- if (!canTransition(this._state, "stopping")) {
120
- throw new RuntimeStateError(`Cannot stop runtime from state "${this._state}".`);
226
+ if (!canStop(this._state)) {
227
+ throw new RuntimeStateError(`Cannot stop a runtime in state "${this._state}"; stop is valid from "created", "running" and "failed".`);
121
228
  }
122
229
  this.stopPromise = this.performStop();
123
230
  try {
@@ -132,26 +239,34 @@ export class DefaultRuntime {
132
239
  */
133
240
  async performStart() {
134
241
  this.transitionTo("initializing");
242
+ this.readinessTracker.setState("initializing", "Runtime is starting its modules.");
135
243
  if (this.options.emitEvents) {
136
244
  this.emitEvent("runtime.initializing");
137
245
  }
246
+ // Registered before startup rather than after: a SIGTERM arriving
247
+ // while modules are still coming up must be handled, not ignored.
248
+ this.signalHandler.register(() => this.handleShutdownSignal());
138
249
  try {
139
- await executeStartup(this.lifecycle, this.options.runtimeId, this._context.eventBus, this.logger, this.options.emitEvents);
250
+ await executeStartup(this.lifecycle, this.options.runtimeId, this._contextBase.eventBus, this.logger, this.options.emitEvents, this.options.startupTimeout);
140
251
  this.transitionTo("running");
141
- this._ready = true;
142
252
  this._startedAt = new Date();
143
- this.readinessTracker.markReady("Runtime started successfully.");
253
+ // Evaluate any checks registered before startup instead of
254
+ // declaring readiness over the top of them. Force-marking ready
255
+ // here reported a runtime as ready while a dependency check was
256
+ // failing.
257
+ await this.readinessTracker.runChecks();
258
+ if (this.readinessTracker.hasChecks()) {
259
+ if (!this.ready) {
260
+ this.logger.warn("Runtime started, but one or more readiness checks are failing.", { reason: this.readinessTracker.getState().reason });
261
+ }
262
+ }
263
+ else {
264
+ this.readinessTracker.markReady("Runtime started successfully.");
265
+ }
144
266
  if (this.options.emitEvents) {
145
267
  this.emitEvent("runtime.running");
146
- this.emitEvent("runtime.readiness.changed", createReadinessEventPayload(this.options.runtimeId, "running", true, "Runtime started successfully."));
268
+ this.emitEvent("runtime.readiness.changed", createReadinessEventPayload(this.options.runtimeId, "running", this.ready, this.readinessTracker.getState().reason));
147
269
  }
148
- this.signalHandler.register(() => {
149
- this.stop().catch((error) => {
150
- this.logger.error("Shutdown failed.", {
151
- errorMessage: error.message,
152
- });
153
- });
154
- });
155
270
  this.logger.info("Runtime is ready.", {
156
271
  runtimeId: this.options.runtimeId,
157
272
  environment: this.options.environment,
@@ -168,7 +283,12 @@ export class DefaultRuntime {
168
283
  this.emitEvent("runtime.failed", createFailureEventPayload(this.options.runtimeId, "failed", runtimeError, "startup"));
169
284
  }
170
285
  try {
171
- await rollbackStartup(this.lifecycle, this.logger);
286
+ const rollbackFailures = await rollbackStartup(this.lifecycle, this.logger);
287
+ if (rollbackFailures.length > 0) {
288
+ this.logger.error("Rollback completed with failures.", {
289
+ failedModules: rollbackFailures.map((failure) => failure.moduleId),
290
+ });
291
+ }
172
292
  }
173
293
  catch (rollbackError) {
174
294
  this.logger.error("Rollback failed.", {
@@ -186,22 +306,31 @@ export class DefaultRuntime {
186
306
  */
187
307
  async performStop() {
188
308
  this.transitionTo("stopping");
189
- this._ready = false;
190
- this.readinessTracker.markNotReady("Runtime is shutting down.");
309
+ this.readinessTracker.setState("shutting_down", "Runtime is shutting down.");
191
310
  if (this.options.emitEvents) {
192
311
  this.emitEvent("runtime.stopping");
193
312
  }
194
313
  try {
195
- await executeShutdown(this.lifecycle, this.options.runtimeId, this._context.eventBus, this.logger, this.options.shutdownTimeout, this.options.emitEvents);
314
+ const result = await executeShutdown(this.lifecycle, this.options.runtimeId, this._contextBase.eventBus, this.logger, this.options.shutdownTimeout, this.options.emitEvents);
315
+ this._shutdownFailures = result.failures;
196
316
  this.transitionTo("stopped");
197
317
  this._stoppedAt = new Date();
198
- this.signalHandler.unregister();
199
318
  if (this.options.emitEvents) {
200
319
  this.emitEvent("runtime.stopped");
201
320
  }
202
- this.logger.info("Runtime stopped.", {
203
- runtimeId: this.options.runtimeId,
204
- });
321
+ if (result.failures.length > 0) {
322
+ // A teardown that dropped modules on the floor must not read as
323
+ // a clean stop; `status.shutdownFailures` records what failed.
324
+ this.logger.warn("Runtime stopped with module failures.", {
325
+ runtimeId: this.options.runtimeId,
326
+ failedModules: result.failures.map((failure) => failure.moduleId),
327
+ });
328
+ }
329
+ else {
330
+ this.logger.info("Runtime stopped.", {
331
+ runtimeId: this.options.runtimeId,
332
+ });
333
+ }
205
334
  }
206
335
  catch (error) {
207
336
  const runtimeError = toRuntimeError(error, "shutdown");
@@ -216,31 +345,69 @@ export class DefaultRuntime {
216
345
  this.transitionTo("failed");
217
346
  throw runtimeError;
218
347
  }
348
+ finally {
349
+ // Released on both paths: leaving handlers attached after a failed
350
+ // stop keeps the process listening for a signal it can no longer
351
+ // act on.
352
+ this.signalHandler.unregister();
353
+ }
354
+ }
355
+ /**
356
+ * Runs shutdown in response to a termination signal.
357
+ */
358
+ async handleShutdownSignal() {
359
+ try {
360
+ await this.stop();
361
+ }
362
+ catch (error) {
363
+ this.logger.error("Shutdown failed.", {
364
+ errorMessage: error instanceof Error ? error.message : String(error),
365
+ });
366
+ }
219
367
  }
220
368
  /**
221
369
  * Transitions to a new state.
222
370
  */
223
371
  transitionTo(newState) {
224
372
  const oldState = this._state;
225
- if (!canTransition(oldState, newState)) {
226
- throw new RuntimeStateError(`Invalid state transition from "${oldState}" to "${newState}".`);
227
- }
373
+ // Delegates to the exported assertion so the public state machine and
374
+ // the runtime cannot disagree about what a legal transition is.
375
+ assertTransition(oldState, newState);
376
+ const previousHealth = this.health.state;
228
377
  this._state = newState;
229
- this._status = createStatus(newState);
230
378
  this.logger.debug(`Runtime state: ${oldState} -> ${newState}`);
379
+ this.emitHealthChange(previousHealth);
380
+ }
381
+ /**
382
+ * Emits `runtime.health.changed` when the derived health state has moved
383
+ * away from `previousHealth`. Health is derived rather than stored, so
384
+ * callers compare against a value captured before the change.
385
+ */
386
+ emitHealthChange(previousHealth) {
387
+ if (!this.options.emitEvents || !this.options.trackHealth) {
388
+ return;
389
+ }
390
+ const health = this.health;
391
+ if (health.state === previousHealth) {
392
+ return;
393
+ }
394
+ this.emitEvent("runtime.health.changed", createHealthEventPayload(this.options.runtimeId, this._state, previousHealth, health.state, health.checks.map((check) => ({
395
+ name: check.name,
396
+ healthy: check.healthy,
397
+ }))));
231
398
  }
232
399
  /**
233
400
  * Emits a runtime event.
234
401
  */
235
402
  emitEvent(eventType, payload) {
236
- if (this.options.emitEvents && this._context.eventBus) {
403
+ if (this.options.emitEvents && this._contextBase.eventBus) {
237
404
  const eventPayload = payload ??
238
405
  createRuntimeEventPayload(this.options.runtimeId, this._state);
239
406
  const event = createEvent({
240
407
  type: eventType,
241
408
  payload: eventPayload,
242
409
  });
243
- this._context.eventBus.publish(event);
410
+ publishRuntimeEvent(this._contextBase.eventBus, this.logger, event);
244
411
  }
245
412
  }
246
413
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Runtime context providing access to running application state.
3
3
  */
4
- export { createRuntimeContext, createRuntimeIdentity, } from "./runtimeContext.core.js";
4
+ export { createRuntimeContext, withRuntimeContextState, } from "./runtimeContext.core.js";
5
5
  export { createRuntimeId, createCorrelationId, createRequestId, } from "./runtimeContext.factory.js";
6
- export type { RuntimeContext, RuntimeIdentity, RuntimeContextDependencies, RuntimeContextState, } from "./runtimeContext.type.js";
6
+ export type { RuntimeContext, RuntimeContextDependencies, RuntimeContextState, } from "./runtimeContext.type.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Runtime context providing access to running application state.
3
3
  */
4
- export { createRuntimeContext, createRuntimeIdentity, } from "./runtimeContext.core.js";
4
+ export { createRuntimeContext, withRuntimeContextState, } from "./runtimeContext.core.js";
5
5
  export { createRuntimeId, createCorrelationId, createRequestId, } from "./runtimeContext.factory.js";
6
6
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,20 @@
1
- import type { Environment } from "@zudojs/constants";
2
- import type { RuntimeContext, RuntimeContextDependencies, RuntimeIdentity } from "./runtimeContext.type.js";
1
+ import type { RuntimeContext, RuntimeContextDependencies, RuntimeContextState } from "./runtimeContext.type.js";
3
2
  /**
4
3
  * Creates a runtime context.
5
4
  */
6
5
  export declare function createRuntimeContext(dependencies: RuntimeContextDependencies): RuntimeContext;
7
6
  /**
8
- * Creates runtime identity information.
7
+ * Returns a context reflecting the current runtime state.
8
+ *
9
+ * `createRuntimeContext` captures the dependencies that never change for
10
+ * the life of a runtime; the lifecycle-dependent fields are layered on top
11
+ * of that base each time the context is read, so callers never observe a
12
+ * stale snapshot.
13
+ *
14
+ * Every field of {@link RuntimeContextState} is applied. Previously only
15
+ * `startedAt` was, so `stoppedAt`, `failedAt` and `error` were computed by
16
+ * the runtime, passed in here, and silently dropped — the context claimed
17
+ * a clean runtime on the failure path.
9
18
  */
10
- export declare function createRuntimeIdentity(runtimeId: string, environment: Environment, applicationName: string, applicationVersion: string): RuntimeIdentity;
19
+ export declare function withRuntimeContextState(base: RuntimeContext, state: RuntimeContextState): RuntimeContext;
11
20
  //# sourceMappingURL=runtimeContext.core.d.ts.map
@@ -1,4 +1,3 @@
1
- import { createRuntimeId as generateRuntimeId } from "./runtimeContext.factory.js";
2
1
  import { createStatus } from "../runtimeState/runtimeState.core.js";
3
2
  /**
4
3
  * Creates a runtime context.
@@ -21,21 +20,33 @@ export function createRuntimeContext(dependencies) {
21
20
  timestamp: new Date(),
22
21
  },
23
22
  ready: false,
23
+ metadata: dependencies.metadata ?? Object.freeze({}),
24
24
  });
25
25
  }
26
26
  /**
27
- * Creates runtime identity information.
27
+ * Returns a context reflecting the current runtime state.
28
+ *
29
+ * `createRuntimeContext` captures the dependencies that never change for
30
+ * the life of a runtime; the lifecycle-dependent fields are layered on top
31
+ * of that base each time the context is read, so callers never observe a
32
+ * stale snapshot.
33
+ *
34
+ * Every field of {@link RuntimeContextState} is applied. Previously only
35
+ * `startedAt` was, so `stoppedAt`, `failedAt` and `error` were computed by
36
+ * the runtime, passed in here, and silently dropped — the context claimed
37
+ * a clean runtime on the failure path.
28
38
  */
29
- export function createRuntimeIdentity(runtimeId, environment, applicationName, applicationVersion) {
39
+ export function withRuntimeContextState(base, state) {
30
40
  return Object.freeze({
31
- runtimeId,
32
- environment,
33
- applicationName,
34
- applicationVersion,
35
- hostname: typeof process !== "undefined"
36
- ? (process.env.HOSTNAME ?? "unknown")
37
- : "unknown",
38
- processId: typeof process !== "undefined" ? process.pid : 0,
41
+ ...base,
42
+ state: state.status.state,
43
+ status: state.status,
44
+ health: state.health,
45
+ ready: state.ready,
46
+ ...(state.startedAt !== undefined && { startedAt: state.startedAt }),
47
+ ...(state.stoppedAt !== undefined && { stoppedAt: state.stoppedAt }),
48
+ ...(state.failedAt !== undefined && { failedAt: state.failedAt }),
49
+ ...(state.error !== undefined && { error: state.error }),
39
50
  });
40
51
  }
41
52
  //# sourceMappingURL=runtimeContext.core.js.map
@@ -1,4 +1,5 @@
1
1
  import type { Environment } from "@zudojs/constants";
2
+ import type { RuntimeError } from "@zudojs/errors";
2
3
  import type { Logger } from "@zudojs/logger";
3
4
  import type { EventBus } from "@zudojs/events";
4
5
  import type { Container } from "@zudojs/container";
@@ -7,6 +8,8 @@ import type { RuntimeId, RuntimeStatus, RuntimeState, RuntimeHealth } from "../r
7
8
  * Runtime context providing access to the running application state.
8
9
  */
9
10
  export interface RuntimeContext {
11
+ /** Additional runtime metadata supplied through runtime options. */
12
+ readonly metadata: Readonly<Record<string, unknown>>;
10
13
  readonly runtimeId: RuntimeId;
11
14
  readonly environment: Environment;
12
15
  readonly applicationName: string;
@@ -14,28 +17,24 @@ export interface RuntimeContext {
14
17
  readonly state: RuntimeState;
15
18
  readonly status: RuntimeStatus;
16
19
  readonly startedAt?: Date;
20
+ /** When the runtime last reached `stopped`. */
21
+ readonly stoppedAt?: Date;
22
+ /** When the runtime last reached `failed`. */
23
+ readonly failedAt?: Date;
24
+ /** The error that failed the runtime, if it has failed. */
25
+ readonly error?: RuntimeError;
17
26
  readonly logger: Logger;
18
27
  readonly container: Container;
19
28
  readonly eventBus: EventBus;
20
29
  readonly health: RuntimeHealth;
21
30
  readonly ready: boolean;
22
31
  }
23
- /**
24
- * Runtime identity information.
25
- */
26
- export interface RuntimeIdentity {
27
- readonly runtimeId: RuntimeId;
28
- readonly applicationName: string;
29
- readonly applicationVersion: string;
30
- readonly environment: Environment;
31
- readonly hostname: string;
32
- readonly processId: number;
33
- readonly startedAt?: Date;
34
- }
35
32
  /**
36
33
  * Dependencies required to create the runtime context.
37
34
  */
38
35
  export interface RuntimeContextDependencies {
36
+ /** Additional runtime metadata, surfaced on the context. */
37
+ readonly metadata?: Readonly<Record<string, unknown>>;
39
38
  readonly runtimeId: RuntimeId;
40
39
  readonly environment: Environment;
41
40
  readonly applicationName: string;
@@ -54,6 +53,6 @@ export interface RuntimeContextState {
54
53
  startedAt?: Date;
55
54
  stoppedAt?: Date;
56
55
  failedAt?: Date;
57
- error?: Error;
56
+ error?: RuntimeError;
58
57
  }
59
58
  //# sourceMappingURL=runtimeContext.type.d.ts.map
@@ -1,4 +1,4 @@
1
- import { RuntimeError, RuntimeStateError as BaseRuntimeStateError, ErrorCode, } from "@zudojs/errors";
1
+ import { RuntimeError, RuntimeStateError as BaseRuntimeStateError, } from "@zudojs/errors";
2
2
  /**
3
3
  * Error thrown when an invalid runtime state is encountered.
4
4
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Runtime lifecycle events.
3
3
  */
4
- export { createRuntimeEventPayload, createModuleEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, } from "./runtimeEvents.core.js";
5
- export type { RuntimeEventType, RuntimeEventPayload, RuntimeModuleEventPayload, RuntimeFailureEventPayload, RuntimeHealthEventPayload, RuntimeReadinessEventPayload, RuntimeEventMap, } from "./runtimeEvents.type.js";
4
+ export { publishRuntimeEvent, createRuntimeEventPayload, createModuleEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, } from "./runtimeEvents.core.js";
5
+ export type { RuntimeEventType, RuntimeModuleEventType, RuntimeEventPayload, RuntimeModuleEventPayload, RuntimeFailureEventPayload, RuntimeHealthEventPayload, RuntimeReadinessEventPayload, RuntimeEventMap, } from "./runtimeEvents.type.js";
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Runtime lifecycle events.
3
3
  */
4
- export { createRuntimeEventPayload, createModuleEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, } from "./runtimeEvents.core.js";
4
+ export { publishRuntimeEvent, createRuntimeEventPayload, createModuleEventPayload, createFailureEventPayload, createHealthEventPayload, createReadinessEventPayload, } from "./runtimeEvents.core.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,5 @@
1
+ import type { Event } from "@zudojs/events";
2
+ import type { RuntimeFailureState, RuntimeState } from "../runtimeState/runtimeState.type.js";
1
3
  import type { RuntimeEventPayload, RuntimeModuleEventPayload, RuntimeFailureEventPayload, RuntimeHealthEventPayload, RuntimeReadinessEventPayload } from "./runtimeEvents.type.js";
2
4
  /**
3
5
  * Creates a base runtime event payload.
@@ -13,7 +15,7 @@ export declare function createModuleEventPayload(runtimeId: string, state: strin
13
15
  /**
14
16
  * Creates a runtime failure event payload.
15
17
  */
16
- export declare function createFailureEventPayload(runtimeId: string, state: string, error: Error, phase: string, failedModuleId?: string): RuntimeFailureEventPayload;
18
+ export declare function createFailureEventPayload(runtimeId: string, state: RuntimeState | RuntimeFailureState, error: Error, phase: string, failedModuleId?: string): RuntimeFailureEventPayload;
17
19
  /**
18
20
  * Creates a runtime health change event payload.
19
21
  */
@@ -25,4 +27,16 @@ export declare function createHealthEventPayload(runtimeId: string, state: strin
25
27
  * Creates a runtime readiness change event payload.
26
28
  */
27
29
  export declare function createReadinessEventPayload(runtimeId: string, state: string, ready: boolean, reason?: string): RuntimeReadinessEventPayload;
30
+ /**
31
+ * Publishes a runtime lifecycle event without letting a failing handler,
32
+ * middleware, or a disposed bus turn into an unhandled promise rejection.
33
+ *
34
+ * Runtime events are informational; a failure to deliver one must never
35
+ * abort startup or shutdown, so the rejection is logged and swallowed.
36
+ */
37
+ export declare function publishRuntimeEvent(eventBus: {
38
+ publish(event: Event): Promise<unknown>;
39
+ }, logger: {
40
+ warn(message: string, context?: Record<string, unknown>): void;
41
+ }, event: Event): void;
28
42
  //# sourceMappingURL=runtimeEvents.core.d.ts.map
@@ -60,4 +60,30 @@ export function createReadinessEventPayload(runtimeId, state, ready, reason) {
60
60
  ...(reason !== undefined && { reason }),
61
61
  });
62
62
  }
63
+ /**
64
+ * Publishes a runtime lifecycle event without letting a failing handler,
65
+ * middleware, or a disposed bus turn into an unhandled promise rejection.
66
+ *
67
+ * Runtime events are informational; a failure to deliver one must never
68
+ * abort startup or shutdown, so the rejection is logged and swallowed.
69
+ */
70
+ export function publishRuntimeEvent(eventBus, logger, event) {
71
+ let result;
72
+ try {
73
+ result = eventBus.publish(event);
74
+ }
75
+ catch (error) {
76
+ logger.warn("Failed to publish runtime event.", {
77
+ eventType: event.type,
78
+ error: error instanceof Error ? error.message : String(error),
79
+ });
80
+ return;
81
+ }
82
+ void Promise.resolve(result).catch((error) => {
83
+ logger.warn("Failed to publish runtime event.", {
84
+ eventType: event.type,
85
+ error: error instanceof Error ? error.message : String(error),
86
+ });
87
+ });
88
+ }
63
89
  //# sourceMappingURL=runtimeEvents.core.js.map