@zudojs/runtime 0.0.1 → 1.0.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 (127) 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 +109 -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 +199 -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.js +8 -2
  38. package/dist/runtimeOptions/runtimeOptions.type.d.ts +29 -1
  39. package/dist/runtimeOptions/runtimeOptions.type.js +6 -0
  40. package/dist/runtimeState/index.d.ts +1 -1
  41. package/dist/runtimeState/runtimeState.core.d.ts +4 -0
  42. package/dist/runtimeState/runtimeState.core.js +17 -4
  43. package/dist/runtimeState/runtimeState.type.d.ts +14 -17
  44. package/dist/shutdown/shutdown.core.d.ts +20 -1
  45. package/dist/shutdown/shutdown.core.js +55 -29
  46. package/dist/signalHandler/signalHandler.core.d.ts +53 -13
  47. package/dist/signalHandler/signalHandler.core.js +97 -27
  48. package/dist/startup/startup.core.d.ts +3 -2
  49. package/dist/startup/startup.core.js +52 -46
  50. package/dist/testRuntime/index.d.ts +6 -1
  51. package/dist/testRuntime/index.js +5 -1
  52. package/dist/testRuntime/testRuntime.core.d.ts +21 -2
  53. package/dist/testRuntime/testRuntime.core.js +23 -6
  54. package/package.json +29 -22
  55. package/dist/.tsbuildinfo +0 -1
  56. package/dist/dependencyGraph/dependencyGraph.core.d.ts.map +0 -1
  57. package/dist/dependencyGraph/dependencyGraph.core.js.map +0 -1
  58. package/dist/dependencyGraph/dependencyGraph.type.d.ts.map +0 -1
  59. package/dist/dependencyGraph/dependencyGraph.type.js.map +0 -1
  60. package/dist/dependencyGraph/index.d.ts.map +0 -1
  61. package/dist/dependencyGraph/index.js.map +0 -1
  62. package/dist/index.d.ts.map +0 -1
  63. package/dist/index.js.map +0 -1
  64. package/dist/lifecycle/index.d.ts.map +0 -1
  65. package/dist/lifecycle/index.js.map +0 -1
  66. package/dist/lifecycle/lifecycle.core.d.ts.map +0 -1
  67. package/dist/lifecycle/lifecycle.core.js.map +0 -1
  68. package/dist/lifecycle/lifecycle.type.d.ts.map +0 -1
  69. package/dist/lifecycle/lifecycle.type.js.map +0 -1
  70. package/dist/readiness/index.d.ts.map +0 -1
  71. package/dist/readiness/index.js.map +0 -1
  72. package/dist/readiness/readiness.core.d.ts.map +0 -1
  73. package/dist/readiness/readiness.core.js.map +0 -1
  74. package/dist/readiness/readiness.type.d.ts.map +0 -1
  75. package/dist/readiness/readiness.type.js.map +0 -1
  76. package/dist/registry/index.d.ts.map +0 -1
  77. package/dist/registry/index.js.map +0 -1
  78. package/dist/registry/registry.core.d.ts.map +0 -1
  79. package/dist/registry/registry.core.js.map +0 -1
  80. package/dist/runtime/runtime.core.d.ts.map +0 -1
  81. package/dist/runtime/runtime.core.js.map +0 -1
  82. package/dist/runtimeContext/index.d.ts.map +0 -1
  83. package/dist/runtimeContext/index.js.map +0 -1
  84. package/dist/runtimeContext/runtimeContext.core.d.ts.map +0 -1
  85. package/dist/runtimeContext/runtimeContext.core.js.map +0 -1
  86. package/dist/runtimeContext/runtimeContext.factory.d.ts.map +0 -1
  87. package/dist/runtimeContext/runtimeContext.factory.js.map +0 -1
  88. package/dist/runtimeContext/runtimeContext.type.d.ts.map +0 -1
  89. package/dist/runtimeContext/runtimeContext.type.js.map +0 -1
  90. package/dist/runtimeError/index.d.ts.map +0 -1
  91. package/dist/runtimeError/index.js.map +0 -1
  92. package/dist/runtimeError/runtimeError.base.d.ts.map +0 -1
  93. package/dist/runtimeError/runtimeError.base.js.map +0 -1
  94. package/dist/runtimeEvents/index.d.ts.map +0 -1
  95. package/dist/runtimeEvents/index.js.map +0 -1
  96. package/dist/runtimeEvents/runtimeEvents.core.d.ts.map +0 -1
  97. package/dist/runtimeEvents/runtimeEvents.core.js.map +0 -1
  98. package/dist/runtimeEvents/runtimeEvents.type.d.ts.map +0 -1
  99. package/dist/runtimeEvents/runtimeEvents.type.js.map +0 -1
  100. package/dist/runtimeOptions/index.d.ts.map +0 -1
  101. package/dist/runtimeOptions/index.js.map +0 -1
  102. package/dist/runtimeOptions/runtimeOptions.core.d.ts.map +0 -1
  103. package/dist/runtimeOptions/runtimeOptions.core.js.map +0 -1
  104. package/dist/runtimeOptions/runtimeOptions.type.d.ts.map +0 -1
  105. package/dist/runtimeOptions/runtimeOptions.type.js.map +0 -1
  106. package/dist/runtimeState/index.d.ts.map +0 -1
  107. package/dist/runtimeState/index.js.map +0 -1
  108. package/dist/runtimeState/runtimeState.core.d.ts.map +0 -1
  109. package/dist/runtimeState/runtimeState.core.js.map +0 -1
  110. package/dist/runtimeState/runtimeState.type.d.ts.map +0 -1
  111. package/dist/runtimeState/runtimeState.type.js.map +0 -1
  112. package/dist/shutdown/index.d.ts.map +0 -1
  113. package/dist/shutdown/index.js.map +0 -1
  114. package/dist/shutdown/shutdown.core.d.ts.map +0 -1
  115. package/dist/shutdown/shutdown.core.js.map +0 -1
  116. package/dist/signalHandler/index.d.ts.map +0 -1
  117. package/dist/signalHandler/index.js.map +0 -1
  118. package/dist/signalHandler/signalHandler.core.d.ts.map +0 -1
  119. package/dist/signalHandler/signalHandler.core.js.map +0 -1
  120. package/dist/startup/index.d.ts.map +0 -1
  121. package/dist/startup/index.js.map +0 -1
  122. package/dist/startup/startup.core.d.ts.map +0 -1
  123. package/dist/startup/startup.core.js.map +0 -1
  124. package/dist/testRuntime/index.d.ts.map +0 -1
  125. package/dist/testRuntime/index.js.map +0 -1
  126. package/dist/testRuntime/testRuntime.core.d.ts.map +0 -1
  127. 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;
@@ -114,10 +209,11 @@ export class DefaultRuntime {
114
209
  if (this._state === "created") {
115
210
  this._state = "stopped";
116
211
  this._stoppedAt = new Date();
212
+ this.signalHandler.unregister();
117
213
  return;
118
214
  }
119
- if (!canTransition(this._state, "stopping")) {
120
- throw new RuntimeStateError(`Cannot stop runtime from state "${this._state}".`);
215
+ if (!canStop(this._state)) {
216
+ throw new RuntimeStateError(`Cannot stop a runtime in state "${this._state}"; stop is valid from "created", "running" and "failed".`);
121
217
  }
122
218
  this.stopPromise = this.performStop();
123
219
  try {
@@ -132,26 +228,34 @@ export class DefaultRuntime {
132
228
  */
133
229
  async performStart() {
134
230
  this.transitionTo("initializing");
231
+ this.readinessTracker.setState("initializing", "Runtime is starting its modules.");
135
232
  if (this.options.emitEvents) {
136
233
  this.emitEvent("runtime.initializing");
137
234
  }
235
+ // Registered before startup rather than after: a SIGTERM arriving
236
+ // while modules are still coming up must be handled, not ignored.
237
+ this.signalHandler.register(() => this.handleShutdownSignal());
138
238
  try {
139
- await executeStartup(this.lifecycle, this.options.runtimeId, this._context.eventBus, this.logger, this.options.emitEvents);
239
+ await executeStartup(this.lifecycle, this.options.runtimeId, this._contextBase.eventBus, this.logger, this.options.emitEvents, this.options.startupTimeout);
140
240
  this.transitionTo("running");
141
- this._ready = true;
142
241
  this._startedAt = new Date();
143
- this.readinessTracker.markReady("Runtime started successfully.");
242
+ // Evaluate any checks registered before startup instead of
243
+ // declaring readiness over the top of them. Force-marking ready
244
+ // here reported a runtime as ready while a dependency check was
245
+ // failing.
246
+ await this.readinessTracker.runChecks();
247
+ if (this.readinessTracker.hasChecks()) {
248
+ if (!this.ready) {
249
+ this.logger.warn("Runtime started, but one or more readiness checks are failing.", { reason: this.readinessTracker.getState().reason });
250
+ }
251
+ }
252
+ else {
253
+ this.readinessTracker.markReady("Runtime started successfully.");
254
+ }
144
255
  if (this.options.emitEvents) {
145
256
  this.emitEvent("runtime.running");
146
- this.emitEvent("runtime.readiness.changed", createReadinessEventPayload(this.options.runtimeId, "running", true, "Runtime started successfully."));
257
+ this.emitEvent("runtime.readiness.changed", createReadinessEventPayload(this.options.runtimeId, "running", this.ready, this.readinessTracker.getState().reason));
147
258
  }
148
- this.signalHandler.register(() => {
149
- this.stop().catch((error) => {
150
- this.logger.error("Shutdown failed.", {
151
- errorMessage: error.message,
152
- });
153
- });
154
- });
155
259
  this.logger.info("Runtime is ready.", {
156
260
  runtimeId: this.options.runtimeId,
157
261
  environment: this.options.environment,
@@ -168,7 +272,12 @@ export class DefaultRuntime {
168
272
  this.emitEvent("runtime.failed", createFailureEventPayload(this.options.runtimeId, "failed", runtimeError, "startup"));
169
273
  }
170
274
  try {
171
- await rollbackStartup(this.lifecycle, this.logger);
275
+ const rollbackFailures = await rollbackStartup(this.lifecycle, this.logger);
276
+ if (rollbackFailures.length > 0) {
277
+ this.logger.error("Rollback completed with failures.", {
278
+ failedModules: rollbackFailures.map((failure) => failure.moduleId),
279
+ });
280
+ }
172
281
  }
173
282
  catch (rollbackError) {
174
283
  this.logger.error("Rollback failed.", {
@@ -186,22 +295,31 @@ export class DefaultRuntime {
186
295
  */
187
296
  async performStop() {
188
297
  this.transitionTo("stopping");
189
- this._ready = false;
190
- this.readinessTracker.markNotReady("Runtime is shutting down.");
298
+ this.readinessTracker.setState("shutting_down", "Runtime is shutting down.");
191
299
  if (this.options.emitEvents) {
192
300
  this.emitEvent("runtime.stopping");
193
301
  }
194
302
  try {
195
- await executeShutdown(this.lifecycle, this.options.runtimeId, this._context.eventBus, this.logger, this.options.shutdownTimeout, this.options.emitEvents);
303
+ const result = await executeShutdown(this.lifecycle, this.options.runtimeId, this._contextBase.eventBus, this.logger, this.options.shutdownTimeout, this.options.emitEvents);
304
+ this._shutdownFailures = result.failures;
196
305
  this.transitionTo("stopped");
197
306
  this._stoppedAt = new Date();
198
- this.signalHandler.unregister();
199
307
  if (this.options.emitEvents) {
200
308
  this.emitEvent("runtime.stopped");
201
309
  }
202
- this.logger.info("Runtime stopped.", {
203
- runtimeId: this.options.runtimeId,
204
- });
310
+ if (result.failures.length > 0) {
311
+ // A teardown that dropped modules on the floor must not read as
312
+ // a clean stop; `status.shutdownFailures` records what failed.
313
+ this.logger.warn("Runtime stopped with module failures.", {
314
+ runtimeId: this.options.runtimeId,
315
+ failedModules: result.failures.map((failure) => failure.moduleId),
316
+ });
317
+ }
318
+ else {
319
+ this.logger.info("Runtime stopped.", {
320
+ runtimeId: this.options.runtimeId,
321
+ });
322
+ }
205
323
  }
206
324
  catch (error) {
207
325
  const runtimeError = toRuntimeError(error, "shutdown");
@@ -216,31 +334,69 @@ export class DefaultRuntime {
216
334
  this.transitionTo("failed");
217
335
  throw runtimeError;
218
336
  }
337
+ finally {
338
+ // Released on both paths: leaving handlers attached after a failed
339
+ // stop keeps the process listening for a signal it can no longer
340
+ // act on.
341
+ this.signalHandler.unregister();
342
+ }
343
+ }
344
+ /**
345
+ * Runs shutdown in response to a termination signal.
346
+ */
347
+ async handleShutdownSignal() {
348
+ try {
349
+ await this.stop();
350
+ }
351
+ catch (error) {
352
+ this.logger.error("Shutdown failed.", {
353
+ errorMessage: error instanceof Error ? error.message : String(error),
354
+ });
355
+ }
219
356
  }
220
357
  /**
221
358
  * Transitions to a new state.
222
359
  */
223
360
  transitionTo(newState) {
224
361
  const oldState = this._state;
225
- if (!canTransition(oldState, newState)) {
226
- throw new RuntimeStateError(`Invalid state transition from "${oldState}" to "${newState}".`);
227
- }
362
+ // Delegates to the exported assertion so the public state machine and
363
+ // the runtime cannot disagree about what a legal transition is.
364
+ assertTransition(oldState, newState);
365
+ const previousHealth = this.health.state;
228
366
  this._state = newState;
229
- this._status = createStatus(newState);
230
367
  this.logger.debug(`Runtime state: ${oldState} -> ${newState}`);
368
+ this.emitHealthChange(previousHealth);
369
+ }
370
+ /**
371
+ * Emits `runtime.health.changed` when the derived health state has moved
372
+ * away from `previousHealth`. Health is derived rather than stored, so
373
+ * callers compare against a value captured before the change.
374
+ */
375
+ emitHealthChange(previousHealth) {
376
+ if (!this.options.emitEvents || !this.options.trackHealth) {
377
+ return;
378
+ }
379
+ const health = this.health;
380
+ if (health.state === previousHealth) {
381
+ return;
382
+ }
383
+ this.emitEvent("runtime.health.changed", createHealthEventPayload(this.options.runtimeId, this._state, previousHealth, health.state, health.checks.map((check) => ({
384
+ name: check.name,
385
+ healthy: check.healthy,
386
+ }))));
231
387
  }
232
388
  /**
233
389
  * Emits a runtime event.
234
390
  */
235
391
  emitEvent(eventType, payload) {
236
- if (this.options.emitEvents && this._context.eventBus) {
392
+ if (this.options.emitEvents && this._contextBase.eventBus) {
237
393
  const eventPayload = payload ??
238
394
  createRuntimeEventPayload(this.options.runtimeId, this._state);
239
395
  const event = createEvent({
240
396
  type: eventType,
241
397
  payload: eventPayload,
242
398
  });
243
- this._context.eventBus.publish(event);
399
+ publishRuntimeEvent(this._contextBase.eventBus, this.logger, event);
244
400
  }
245
401
  }
246
402
  }
@@ -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