@zudojs/runtime 1.2.0 → 1.2.1

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.
@@ -50,6 +50,15 @@ export declare class LifecycleManager {
50
50
  * Initializes all modules in dependency order.
51
51
  */
52
52
  initialize(): Promise<LifecycleResult>;
53
+ /**
54
+ * Returns the first declared dependency of `moduleId` that is known to
55
+ * have failed or been skipped, or `undefined` when none has.
56
+ *
57
+ * Only direct dependencies are inspected: `blocked` already contains
58
+ * every module skipped by an earlier group, and groups are visited in
59
+ * dependency order, so the cascade is transitive.
60
+ */
61
+ private findBlockingDependency;
53
62
  /**
54
63
  * Initializes a single module, converting a throw into a failure.
55
64
  */
@@ -116,14 +116,44 @@ export class LifecycleManager {
116
116
  const groups = this.options.parallelInitialization
117
117
  ? depGraph.parallelGroups
118
118
  : depGraph.order.map((moduleId) => [moduleId]);
119
+ // Ids that must not have dependents initialized on top of them:
120
+ // modules whose own hook threw, plus modules already skipped for the
121
+ // same reason, so the skip cascades transitively. `continueOnFailure`
122
+ // used to consult only the failure COUNT, so a dependent of a broken
123
+ // module was initialized, readied, and reported as started — an API
124
+ // serving traffic against a database that never came up.
125
+ const blocked = new Set();
119
126
  for (const group of groups) {
120
127
  if (this.cancellation.isCancelled) {
121
128
  break;
122
129
  }
123
- const results = await Promise.all(group.map((moduleId) => this.initializeModule(moduleId)));
130
+ const runnable = [];
131
+ for (const moduleId of group) {
132
+ const blocker = this.findBlockingDependency(moduleId, blocked);
133
+ if (blocker === undefined) {
134
+ runnable.push(moduleId);
135
+ continue;
136
+ }
137
+ const failure = {
138
+ moduleId,
139
+ phase: "initialize",
140
+ error: new RuntimeStartError(`Module "${moduleId}" was not initialized because its ` +
141
+ `dependency "${blocker}" failed.`, { phase: "initialize", failedModuleId: blocker }),
142
+ durationMs: 0,
143
+ };
144
+ failed.push(failure);
145
+ blocked.add(moduleId);
146
+ this.logger.error(`Module "${moduleId}" was skipped because its dependency "${blocker}" failed.`);
147
+ this.emitModuleEvent("runtime.module.failed", moduleId, "failed", {
148
+ durationMs: 0,
149
+ error: failure.error,
150
+ });
151
+ }
152
+ const results = await Promise.all(runnable.map((moduleId) => this.initializeModule(moduleId)));
124
153
  for (const result of results) {
125
154
  if (result.failure) {
126
155
  failed.push(result.failure);
156
+ blocked.add(result.moduleId);
127
157
  }
128
158
  else if (!result.abandoned) {
129
159
  succeeded.push(result.moduleId);
@@ -140,6 +170,18 @@ export class LifecycleManager {
140
170
  durationMs: Date.now() - startTime,
141
171
  });
142
172
  }
173
+ /**
174
+ * Returns the first declared dependency of `moduleId` that is known to
175
+ * have failed or been skipped, or `undefined` when none has.
176
+ *
177
+ * Only direct dependencies are inspected: `blocked` already contains
178
+ * every module skipped by an earlier group, and groups are visited in
179
+ * dependency order, so the cascade is transitive.
180
+ */
181
+ findBlockingDependency(moduleId, blocked) {
182
+ const dependencies = this.modules.get(moduleId)?.dependencies ?? [];
183
+ return dependencies.find((dependency) => blocked.has(dependency));
184
+ }
143
185
  /**
144
186
  * Initializes a single module, converting a throw into a failure.
145
187
  */
@@ -79,6 +79,14 @@ export interface LifecycleManagerOptions {
79
79
  * closing the same resources.
80
80
  */
81
81
  readonly shutdownTimeout?: number;
82
+ /**
83
+ * Keeps initializing and starting after a module fails.
84
+ *
85
+ * Only modules INDEPENDENT of the failure continue: a module that
86
+ * declares a failed (or already skipped) module in `dependencies` is
87
+ * never initialized or readied, and is reported in `failed` with the
88
+ * blocking dependency named. Defaults to false.
89
+ */
82
90
  readonly continueOnFailure?: boolean;
83
91
  /**
84
92
  * Receives one event per module per lifecycle phase.
@@ -4,6 +4,7 @@
4
4
  * Manages multiple runtime instances for scenarios where Zudojs
5
5
  * runs multiple applications or workers in a single process.
6
6
  */
7
+ import { RuntimeStateError } from "../runtimeError/index.js";
7
8
  /**
8
9
  * Runtime registry for managing multiple runtime instances.
9
10
  *
@@ -19,7 +20,7 @@ export class RuntimeRegistry {
19
20
  */
20
21
  register(id, runtime) {
21
22
  if (this.runtimes.has(id)) {
22
- throw new Error(`Runtime "${id}" is already registered.`);
23
+ throw new RuntimeStateError(`Runtime "${id}" is already registered.`);
23
24
  }
24
25
  this.runtimes.set(id, runtime);
25
26
  }
@@ -56,7 +57,7 @@ export class RuntimeRegistry {
56
57
  require(id) {
57
58
  const runtime = this.runtimes.get(id);
58
59
  if (!runtime) {
59
- throw new Error(`Runtime "${id}" not found.`);
60
+ throw new RuntimeStateError(`Runtime "${id}" not found.`);
60
61
  }
61
62
  return runtime;
62
63
  }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_RUNTIME_OPTIONS } from "./runtimeOptions.type.js";
2
2
  import { createRuntimeId } from "../runtimeContext/runtimeContext.factory.js";
3
+ import { RuntimeError } from "@zudojs/errors";
3
4
  /**
4
5
  * Resolves runtime options with defaults applied.
5
6
  *
@@ -24,22 +25,40 @@ export function resolveRuntimeOptions(options) {
24
25
  */
25
26
  export function validateRuntimeOptions(options) {
26
27
  if (!options.environment) {
27
- throw new Error("Runtime environment is required.");
28
+ throw new RuntimeError("Runtime environment is required.", {
29
+ metadata: { option: "environment" },
30
+ });
28
31
  }
29
32
  if (!options.applicationName) {
30
- throw new Error("Application name is required.");
33
+ throw new RuntimeError("Application name is required.", {
34
+ metadata: { option: "applicationName" },
35
+ });
31
36
  }
32
37
  if (options.shutdownTimeout <= 0) {
33
- throw new Error("Shutdown timeout must be positive.");
38
+ throw new RuntimeError("Shutdown timeout must be positive.", {
39
+ metadata: { option: "shutdownTimeout", value: options.shutdownTimeout },
40
+ });
34
41
  }
35
42
  if (options.startupTimeout <= 0) {
36
- throw new Error("Startup timeout must be positive.");
43
+ throw new RuntimeError("Startup timeout must be positive.", {
44
+ metadata: { option: "startupTimeout", value: options.startupTimeout },
45
+ });
37
46
  }
38
47
  if (!Number.isFinite(options.fatalExitTimeout) || options.fatalExitTimeout < 0) {
39
- throw new Error(`Fatal exit timeout must be a finite, non-negative number, got ${options.fatalExitTimeout}.`);
48
+ throw new RuntimeError(`Fatal exit timeout must be a finite, non-negative number, got ${options.fatalExitTimeout}.`, {
49
+ metadata: {
50
+ option: "fatalExitTimeout",
51
+ value: options.fatalExitTimeout,
52
+ },
53
+ });
40
54
  }
41
55
  if (options.readinessCheckTimeout < 0) {
42
- throw new Error(`Readiness check timeout must be zero or positive, got ${options.readinessCheckTimeout}. Use 0 to run checks without a bound.`);
56
+ throw new RuntimeError(`Readiness check timeout must be zero or positive, got ${options.readinessCheckTimeout}. Use 0 to run checks without a bound.`, {
57
+ metadata: {
58
+ option: "readinessCheckTimeout",
59
+ value: options.readinessCheckTimeout,
60
+ },
61
+ });
43
62
  }
44
63
  }
45
64
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/runtime",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Application lifecycle orchestrator with dependency ordering, rollback, signals, and readiness checks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,12 +27,12 @@
27
27
  "vitest": "^4.1.11"
28
28
  },
29
29
  "dependencies": {
30
- "@zudojs/constants": "1.1.0",
31
- "@zudojs/container": "1.1.1",
32
- "@zudojs/core": "1.2.0",
33
- "@zudojs/errors": "1.1.0",
34
- "@zudojs/events": "1.1.0",
35
- "@zudojs/logger": "1.2.0"
30
+ "@zudojs/constants": "1.1.1",
31
+ "@zudojs/container": "1.1.2",
32
+ "@zudojs/core": "1.2.1",
33
+ "@zudojs/errors": "1.2.0",
34
+ "@zudojs/events": "1.2.0",
35
+ "@zudojs/logger": "1.3.0"
36
36
  },
37
37
  "license": "MIT",
38
38
  "author": {