@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,10 +1,11 @@
1
+ import type { RuntimeFailureState, RuntimeState } from "../runtimeState/runtimeState.type.js";
1
2
  /**
2
3
  * Runtime lifecycle events.
3
4
  *
4
5
  * These events are emitted through the EventBus when
5
6
  * the runtime transitions through its lifecycle.
6
7
  */
7
- export type RuntimeEventType = "runtime.created" | "runtime.initializing" | "runtime.initialized" | "runtime.starting" | "runtime.running" | "runtime.stopping" | "runtime.stopped" | "runtime.failed" | "runtime.module.initializing" | "runtime.module.initialized" | "runtime.module.starting" | "runtime.module.started" | "runtime.module.stopping" | "runtime.module.stopped" | "runtime.module.failed" | "runtime.shutdown.drain" | "runtime.shutdown.complete" | "runtime.health.changed" | "runtime.readiness.changed";
8
+ export type RuntimeEventType = "runtime.initializing" | "runtime.running" | "runtime.stopping" | "runtime.stopped" | "runtime.failed" | "runtime.module.initializing" | "runtime.module.initialized" | "runtime.module.starting" | "runtime.module.started" | "runtime.module.stopping" | "runtime.module.stopped" | "runtime.module.failed" | "runtime.shutdown.drain" | "runtime.shutdown.complete" | "runtime.health.changed" | "runtime.readiness.changed";
8
9
  /**
9
10
  * Base payload for all runtime events.
10
11
  */
@@ -26,6 +27,14 @@ export interface RuntimeModuleEventPayload extends RuntimeEventPayload {
26
27
  * Payload for runtime failure events.
27
28
  */
28
29
  export interface RuntimeFailureEventPayload extends RuntimeEventPayload {
30
+ /**
31
+ * The state the runtime was in when it failed.
32
+ *
33
+ * Narrower than the base payload's `state`: this is where
34
+ * {@link RuntimeFailureState} is used, naming which phase failed rather
35
+ * than leaving the reader to parse a free-form string.
36
+ */
37
+ readonly state: RuntimeState | RuntimeFailureState;
29
38
  readonly error: Error;
30
39
  readonly phase: string;
31
40
  readonly failedModuleId?: string;
@@ -48,14 +57,20 @@ export interface RuntimeReadinessEventPayload extends RuntimeEventPayload {
48
57
  readonly ready: boolean;
49
58
  readonly reason?: string;
50
59
  }
60
+ /**
61
+ * Event types carrying a {@link RuntimeModuleEventPayload}.
62
+ */
63
+ export type RuntimeModuleEventType = "runtime.module.initializing" | "runtime.module.initialized" | "runtime.module.starting" | "runtime.module.started" | "runtime.module.stopping" | "runtime.module.stopped" | "runtime.module.failed";
51
64
  /**
52
65
  * Maps event types to their payload types.
66
+ *
67
+ * Every entry here is emitted by the runtime. Entries for events nothing
68
+ * ever published (`runtime.created`, `runtime.initialized`,
69
+ * `runtime.starting`) were removed rather than left as names a consumer
70
+ * could subscribe to and never hear from.
53
71
  */
54
72
  export interface RuntimeEventMap {
55
- "runtime.created": RuntimeEventPayload;
56
73
  "runtime.initializing": RuntimeEventPayload;
57
- "runtime.initialized": RuntimeEventPayload;
58
- "runtime.starting": RuntimeEventPayload;
59
74
  "runtime.running": RuntimeEventPayload;
60
75
  "runtime.stopping": RuntimeEventPayload;
61
76
  "runtime.stopped": RuntimeEventPayload;
@@ -1,6 +1,11 @@
1
1
  import type { ResolvedRuntimeOptions } from "./runtimeOptions.type.js";
2
2
  /**
3
3
  * Resolves runtime options with defaults applied.
4
+ *
5
+ * `runtimeId` is documented as "auto-generated if not provided", but until
6
+ * this generated one nothing did: `DEFAULT_RUNTIME_OPTIONS` has no entry
7
+ * for it, so an omitted id reached every event payload, log line and
8
+ * `runtime.context.runtimeId` as `undefined`.
4
9
  */
5
10
  export declare function resolveRuntimeOptions(options: ResolvedRuntimeOptions): ResolvedRuntimeOptions;
6
11
  /**
@@ -2,11 +2,21 @@ import { DEFAULT_RUNTIME_OPTIONS } from "./runtimeOptions.type.js";
2
2
  import { createRuntimeId } from "../runtimeContext/runtimeContext.factory.js";
3
3
  /**
4
4
  * Resolves runtime options with defaults applied.
5
+ *
6
+ * `runtimeId` is documented as "auto-generated if not provided", but until
7
+ * this generated one nothing did: `DEFAULT_RUNTIME_OPTIONS` has no entry
8
+ * for it, so an omitted id reached every event payload, log line and
9
+ * `runtime.context.runtimeId` as `undefined`.
5
10
  */
6
11
  export function resolveRuntimeOptions(options) {
12
+ // Spreading `options` wholesale lets an explicitly-undefined key erase
13
+ // its default — a common footgun when callers build options with
14
+ // optional fields. Only defined values override.
15
+ const provided = Object.fromEntries(Object.entries(options ?? {}).filter(([, value]) => value !== undefined));
7
16
  return Object.freeze({
8
17
  ...DEFAULT_RUNTIME_OPTIONS,
9
- ...options,
18
+ runtimeId: createRuntimeId(),
19
+ ...provided,
10
20
  });
11
21
  }
12
22
  /**
@@ -25,6 +35,9 @@ export function validateRuntimeOptions(options) {
25
35
  if (options.startupTimeout <= 0) {
26
36
  throw new Error("Startup timeout must be positive.");
27
37
  }
38
+ if (options.readinessCheckTimeout < 0) {
39
+ throw new Error(`Readiness check timeout must be zero or positive, got ${options.readinessCheckTimeout}. Use 0 to run checks without a bound.`);
40
+ }
28
41
  }
29
42
  /**
30
43
  * Creates a runtime options object with all defaults applied.
@@ -52,10 +52,33 @@ export interface RuntimeOptions {
52
52
  */
53
53
  readonly trackReadiness?: boolean;
54
54
  /**
55
- * Whether to enable health tracking.
55
+ * Whether health is derived from readiness checks.
56
+ *
57
+ * When `false`, `runtime.health` reports `unknown` and no
58
+ * `runtime.health.changed` events are emitted; readiness checks still
59
+ * run and `runtime.ready` is unaffected.
60
+ *
56
61
  * @default true
57
62
  */
58
63
  readonly trackHealth?: boolean;
64
+ /**
65
+ * How long a single readiness check may run before it is recorded as
66
+ * failed, in milliseconds. Set to `0` to remove the bound.
67
+ *
68
+ * @default 5000
69
+ */
70
+ readonly readinessCheckTimeout?: number;
71
+ /**
72
+ * Whether modules at the same dependency depth are initialized
73
+ * concurrently.
74
+ *
75
+ * Modules within a depth group do not depend on one another, so this is
76
+ * safe by construction — but it surfaces any ordering a module assumed
77
+ * without declaring, so it is opt-in.
78
+ *
79
+ * @default false
80
+ */
81
+ readonly parallelInitialization?: boolean;
59
82
  /**
60
83
  * Additional runtime metadata.
61
84
  */
@@ -76,6 +99,8 @@ export interface ResolvedRuntimeOptions {
76
99
  readonly emitEvents: boolean;
77
100
  readonly trackReadiness: boolean;
78
101
  readonly trackHealth: boolean;
102
+ readonly readinessCheckTimeout: number;
103
+ readonly parallelInitialization: boolean;
79
104
  readonly metadata: Readonly<Record<string, unknown>>;
80
105
  }
81
106
  /**
@@ -89,6 +114,9 @@ export declare const DEFAULT_RUNTIME_OPTIONS: Readonly<{
89
114
  readonly emitEvents: true;
90
115
  readonly trackReadiness: true;
91
116
  readonly trackHealth: true;
117
+ readonly readinessCheckTimeout: 5000;
118
+ readonly parallelInitialization: false;
92
119
  readonly applicationVersion: "0.1.0";
120
+ readonly metadata: Readonly<{}>;
93
121
  }>;
94
122
  //# sourceMappingURL=runtimeOptions.type.d.ts.map
@@ -9,6 +9,12 @@ export const DEFAULT_RUNTIME_OPTIONS = Object.freeze({
9
9
  emitEvents: true,
10
10
  trackReadiness: true,
11
11
  trackHealth: true,
12
+ readinessCheckTimeout: 5_000,
13
+ parallelInitialization: false,
12
14
  applicationVersion: "0.1.0",
15
+ // `metadata` is required on ResolvedRuntimeOptions, so it needs a
16
+ // default; without one the resolved options claimed a value the
17
+ // runtime never had.
18
+ metadata: Object.freeze({}),
13
19
  });
14
20
  //# sourceMappingURL=runtimeOptions.type.js.map
@@ -2,5 +2,5 @@
2
2
  * Runtime state types and state machine.
3
3
  */
4
4
  export { RUNTIME_STATE_TRANSITIONS, TERMINAL_STATES, STARTABLE_STATES, STOPPABLE_STATES, canTransition, assertTransition, isTerminalState, canStart, canStop, isRunning, hasFailed, createStatus, } from "./runtimeState.core.js";
5
- export type { RuntimeState, RuntimeFailureState, RuntimeStateFull, RuntimeId, RuntimeStatus, RuntimeStateTransition, RuntimeStateTransitions, RuntimeHealthState, RuntimeHealth, RuntimeHealthCheck, } from "./runtimeState.type.js";
5
+ export type { RuntimeState, RuntimeFailureState, RuntimeId, RuntimeStatus, RuntimeShutdownFailure, RuntimeHealthState, RuntimeHealth, RuntimeHealthCheck, } from "./runtimeState.type.js";
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -21,6 +21,10 @@ export declare const STOPPABLE_STATES: readonly RuntimeState[];
21
21
  export declare function canTransition(from: RuntimeState, to: RuntimeState): boolean;
22
22
  /**
23
23
  * Asserts that a state transition is valid.
24
+ *
25
+ * Throws {@link RuntimeStateError} rather than a bare `Error`: every other
26
+ * runtime failure is typed, and a caller that branches on the error type
27
+ * could not catch this one.
24
28
  */
25
29
  export declare function assertTransition(from: RuntimeState, to: RuntimeState): void;
26
30
  /**
@@ -1,15 +1,20 @@
1
+ import { RuntimeStateError } from "../runtimeError/runtimeError.base.js";
1
2
  /**
2
3
  * Defines valid runtime state transitions.
3
4
  */
4
5
  export const RUNTIME_STATE_TRANSITIONS = Object.freeze({
5
- created: ["initializing", "failed"],
6
+ created: ["initializing", "stopped", "failed"],
6
7
  initializing: ["initialized", "running", "failed"],
7
8
  initialized: ["starting", "running", "failed"],
8
9
  starting: ["running", "failed"],
9
10
  running: ["stopping", "failed"],
10
11
  stopping: ["stopped", "failed"],
11
12
  stopped: [],
12
- failed: [],
13
+ // A failed runtime must still be stoppable: startup rollback only
14
+ // reaches modules that were started, so the operator needs a way to
15
+ // release everything else. Without this the failure path — the one
16
+ // most in need of cleanup — is the one with none available.
17
+ failed: ["stopping", "stopped"],
13
18
  });
14
19
  /**
15
20
  * Terminal runtime states.
@@ -29,20 +34,28 @@ export const STARTABLE_STATES = Object.freeze([
29
34
  */
30
35
  export const STOPPABLE_STATES = Object.freeze([
31
36
  "running",
37
+ "failed",
32
38
  ]);
33
39
  /**
34
40
  * Checks whether a state transition is valid.
35
41
  */
36
42
  export function canTransition(from, to) {
37
43
  const allowed = RUNTIME_STATE_TRANSITIONS[from];
38
- return allowed.includes(to);
44
+ return allowed?.includes(to) ?? false;
39
45
  }
40
46
  /**
41
47
  * Asserts that a state transition is valid.
48
+ *
49
+ * Throws {@link RuntimeStateError} rather than a bare `Error`: every other
50
+ * runtime failure is typed, and a caller that branches on the error type
51
+ * could not catch this one.
42
52
  */
43
53
  export function assertTransition(from, to) {
44
54
  if (!canTransition(from, to)) {
45
- throw new Error(`Invalid runtime state transition from "${from}" to "${to}".`);
55
+ throw new RuntimeStateError(`Invalid runtime state transition from "${from}" to "${to}". ` +
56
+ `Valid transitions from "${from}" are: ${RUNTIME_STATE_TRANSITIONS[from].length > 0
57
+ ? RUNTIME_STATE_TRANSITIONS[from].map((s) => `"${s}"`).join(", ")
58
+ : "none — it is a terminal state"}.`);
46
59
  }
47
60
  }
48
61
  /**
@@ -11,10 +11,6 @@ export type RuntimeState = "created" | "initializing" | "initialized" | "startin
11
11
  * Failure-specific runtime states.
12
12
  */
13
13
  export type RuntimeFailureState = "initialization_failed" | "startup_failed" | "shutdown_failed";
14
- /**
15
- * All possible runtime states including failure variants.
16
- */
17
- export type RuntimeStateFull = RuntimeState | RuntimeFailureState;
18
14
  /**
19
15
  * Unique runtime identifier.
20
16
  */
@@ -30,22 +26,24 @@ export interface RuntimeStatus {
30
26
  readonly stoppedAt?: Date;
31
27
  readonly failedAt?: Date;
32
28
  readonly error?: RuntimeError;
29
+ /**
30
+ * Modules that failed to stop or destroy during the last shutdown.
31
+ *
32
+ * A non-empty list means the runtime reached `stopped` without fully
33
+ * releasing its resources — a state that would otherwise be
34
+ * indistinguishable from a clean shutdown.
35
+ */
36
+ readonly shutdownFailures?: readonly RuntimeShutdownFailure[];
33
37
  }
34
38
  /**
35
- * A record of a runtime state transition.
39
+ * A module that failed during shutdown.
36
40
  */
37
- export interface RuntimeStateTransition {
38
- readonly from: RuntimeState;
39
- readonly to: RuntimeState;
40
- readonly timestamp: Date;
41
- readonly reason?: string;
41
+ export interface RuntimeShutdownFailure {
42
+ readonly moduleId: string;
43
+ readonly phase: string;
44
+ readonly error: Error;
45
+ readonly durationMs: number;
42
46
  }
43
- /**
44
- * Defines valid state transitions.
45
- */
46
- export type RuntimeStateTransitions = {
47
- [K in RuntimeState]: readonly RuntimeState[];
48
- };
49
47
  /**
50
48
  * Runtime health state.
51
49
  */
@@ -66,6 +64,5 @@ export interface RuntimeHealthCheck {
66
64
  readonly healthy: boolean;
67
65
  readonly message?: string;
68
66
  readonly durationMs: number;
69
- readonly metadata?: Readonly<Record<string, unknown>>;
70
67
  }
71
68
  //# sourceMappingURL=runtimeState.type.d.ts.map
@@ -1,8 +1,27 @@
1
1
  import type { Logger } from "@zudojs/logger";
2
2
  import type { EventBus } from "@zudojs/events";
3
3
  import { LifecycleManager } from "../lifecycle/index.js";
4
+ import type { LifecycleFailure } from "../lifecycle/lifecycle.type.js";
5
+ /**
6
+ * Result of a shutdown attempt.
7
+ */
8
+ export interface ShutdownResult {
9
+ /** Modules that failed to stop or destroy cleanly. */
10
+ readonly failures: readonly LifecycleFailure[];
11
+ /** Whether the shutdown was cut short by the timeout. */
12
+ readonly timedOut: boolean;
13
+ }
4
14
  /**
5
15
  * Executes the shutdown sequence with timeout.
16
+ *
17
+ * The timeout timer is cleared as soon as the race settles: a timer left
18
+ * armed after a clean shutdown holds the event loop open for its full
19
+ * duration, which is why a stopped process could still take 30 seconds
20
+ * to exit.
21
+ *
22
+ * Module failures are surfaced rather than logged and forgotten — a
23
+ * deployment that failed to release its connections must not look
24
+ * identical to one that shut down cleanly.
6
25
  */
7
- export declare function executeShutdown(lifecycle: LifecycleManager, runtimeId: string, eventBus: EventBus | undefined, logger: Logger, shutdownTimeout: number, emitEvents: boolean): Promise<void>;
26
+ export declare function executeShutdown(lifecycle: LifecycleManager, runtimeId: string, eventBus: EventBus | undefined, logger: Logger, shutdownTimeout: number, emitEvents: boolean): Promise<ShutdownResult>;
8
27
  //# sourceMappingURL=shutdown.core.d.ts.map
@@ -1,70 +1,95 @@
1
1
  import { createEvent } from "@zudojs/events";
2
+ import { createFailureEventPayload, createRuntimeEventPayload, publishRuntimeEvent, } from "../runtimeEvents/index.js";
2
3
  import { LifecycleManager } from "../lifecycle/index.js";
3
4
  import { RuntimeStopError, RuntimeTimeoutError, } from "../runtimeError/index.js";
5
+ /** Largest delay a timer can represent. */
6
+ const MAX_TIMER_DELAY = 2_147_483_647;
4
7
  /**
5
8
  * Executes the shutdown sequence with timeout.
9
+ *
10
+ * The timeout timer is cleared as soon as the race settles: a timer left
11
+ * armed after a clean shutdown holds the event loop open for its full
12
+ * duration, which is why a stopped process could still take 30 seconds
13
+ * to exit.
14
+ *
15
+ * Module failures are surfaced rather than logged and forgotten — a
16
+ * deployment that failed to release its connections must not look
17
+ * identical to one that shut down cleanly.
6
18
  */
7
19
  export async function executeShutdown(lifecycle, runtimeId, eventBus, logger, shutdownTimeout, emitEvents) {
8
20
  if (emitEvents && eventBus) {
9
- eventBus.publish(createEvent({
21
+ publishRuntimeEvent(eventBus, logger, createEvent({
10
22
  type: "runtime.shutdown.drain",
11
- payload: {
12
- runtimeId,
13
- timestamp: new Date(),
14
- state: "stopping",
15
- },
23
+ payload: createRuntimeEventPayload(runtimeId, "stopping"),
16
24
  }));
17
25
  }
18
26
  logger.info("Initiating graceful shutdown.", { timeoutMs: shutdownTimeout });
19
27
  const stopPromise = performShutdown(lifecycle, logger);
28
+ // The shutdown promise keeps running if the timeout wins; attach a
29
+ // handler now so its eventual rejection is never unhandled.
30
+ stopPromise.catch(() => { });
31
+ let timer;
20
32
  const timeoutPromise = new Promise((_, reject) => {
21
- setTimeout(() => {
33
+ timer = setTimeout(() => {
22
34
  reject(new RuntimeTimeoutError("shutdown", shutdownTimeout));
23
- }, shutdownTimeout);
35
+ }, Math.min(Math.max(0, shutdownTimeout), MAX_TIMER_DELAY));
36
+ timer.unref?.();
24
37
  });
25
38
  try {
26
- await Promise.race([stopPromise, timeoutPromise]);
39
+ const failures = await Promise.race([stopPromise, timeoutPromise]);
27
40
  if (emitEvents && eventBus) {
28
- eventBus.publish(createEvent({
41
+ publishRuntimeEvent(eventBus, logger, createEvent({
29
42
  type: "runtime.shutdown.complete",
30
- payload: {
31
- runtimeId,
32
- timestamp: new Date(),
33
- state: "stopped",
34
- },
43
+ payload: createRuntimeEventPayload(runtimeId, "stopped"),
35
44
  }));
36
45
  }
37
- logger.info("Graceful shutdown complete.");
46
+ if (failures.length > 0) {
47
+ logger.warn("Shutdown completed with module failures.", {
48
+ failedModules: failures.map((failure) => failure.moduleId),
49
+ });
50
+ }
51
+ else {
52
+ logger.info("Graceful shutdown complete.");
53
+ }
54
+ return { failures, timedOut: false };
38
55
  }
39
56
  catch (error) {
40
- if (error instanceof RuntimeTimeoutError) {
41
- logger.error("Shutdown timed out, forcing stop.");
57
+ const timedOut = error instanceof RuntimeTimeoutError;
58
+ if (timedOut) {
59
+ logger.error("Shutdown timed out; abandoning remaining module teardown.", { timeoutMs: shutdownTimeout });
42
60
  }
43
61
  if (emitEvents && eventBus) {
44
- eventBus.publish(createEvent({
62
+ publishRuntimeEvent(eventBus, logger, createEvent({
45
63
  type: "runtime.failed",
46
- payload: {
47
- runtimeId,
48
- timestamp: new Date(),
49
- state: "shutdown_failed",
50
- error: error instanceof Error ? error : new Error(String(error)),
51
- phase: "stop",
52
- },
64
+ payload: createFailureEventPayload(runtimeId, "shutdown_failed", error instanceof Error ? error : new Error(String(error)), "stop"),
53
65
  }));
54
66
  }
55
- throw new RuntimeStopError("Runtime shutdown failed.", {
67
+ throw new RuntimeStopError(timedOut
68
+ ? `Runtime shutdown exceeded ${shutdownTimeout}ms.`
69
+ : "Runtime shutdown failed.", {
56
70
  phase: "stop",
57
71
  cause: error instanceof Error ? error : undefined,
58
72
  });
59
73
  }
74
+ finally {
75
+ // Always clear: leaving this armed is what kept a cleanly stopped
76
+ // process alive until the timeout elapsed.
77
+ if (timer !== undefined) {
78
+ clearTimeout(timer);
79
+ }
80
+ }
60
81
  }
61
82
  /**
62
83
  * Performs the actual shutdown operations.
84
+ *
85
+ * Returns every module failure so the caller can decide what a partial
86
+ * teardown means, rather than discarding them at the log line.
63
87
  */
64
88
  async function performShutdown(lifecycle, logger) {
65
- // Stop modules
89
+ const failures = [];
66
90
  const stopResult = await lifecycle.stop();
67
91
  if (stopResult.failed.length > 0) {
92
+ failures.push(...stopResult.failed);
68
93
  logger.warn("Some modules failed during shutdown.", {
69
94
  failedModules: stopResult.failed.map((f) => f.moduleId),
70
95
  });
@@ -73,9 +98,9 @@ async function performShutdown(lifecycle, logger) {
73
98
  modules: stopResult.succeeded,
74
99
  durationMs: stopResult.durationMs,
75
100
  });
76
- // Destroy modules
77
101
  const destroyResult = await lifecycle.destroy();
78
102
  if (destroyResult.failed.length > 0) {
103
+ failures.push(...destroyResult.failed);
79
104
  logger.warn("Some modules failed during destruction.", {
80
105
  failedModules: destroyResult.failed.map((f) => f.moduleId),
81
106
  });
@@ -83,5 +108,6 @@ async function performShutdown(lifecycle, logger) {
83
108
  logger.info("All modules destroyed.", {
84
109
  durationMs: destroyResult.durationMs,
85
110
  });
111
+ return failures;
86
112
  }
87
113
  //# sourceMappingURL=shutdown.core.js.map
@@ -1,44 +1,84 @@
1
1
  import type { Logger } from "@zudojs/logger";
2
+ /**
3
+ * Options controlling signal and fatal-error handling.
4
+ */
5
+ export interface SignalHandlerOptions {
6
+ readonly handleSignals: boolean;
7
+ readonly handleFatalErrors: boolean;
8
+ /**
9
+ * Whether a second termination signal exits immediately.
10
+ *
11
+ * Defaults to `true`. Without it an operator watching a wedged
12
+ * shutdown has no escape short of SIGKILL.
13
+ */
14
+ readonly forceExitOnSecondSignal?: boolean;
15
+ /**
16
+ * Whether an uncaught exception exits the process after shutdown.
17
+ *
18
+ * Defaults to `true`. Installing an `uncaughtException` handler
19
+ * suppresses Node's default crash, so without this the process keeps
20
+ * serving requests on state a fatal error has already made
21
+ * untrustworthy.
22
+ */
23
+ readonly exitOnFatalError?: boolean;
24
+ /**
25
+ * How long a fatal-error shutdown may take before the process exits
26
+ * anyway, in milliseconds. Defaults to 10000.
27
+ */
28
+ readonly fatalExitTimeout?: number;
29
+ /** Exit hook, injected for testing. Defaults to `process.exit`. */
30
+ readonly exit?: (code: number) => void;
31
+ }
2
32
  /**
3
33
  * Signal handler for process lifecycle events.
4
34
  */
5
35
  export declare class SignalHandler {
6
36
  private readonly logger;
7
- private readonly handleSignals;
8
- private readonly handleFatalErrors;
37
+ private readonly options;
9
38
  private shutdownHandler;
10
39
  private isShuttingDown;
11
- constructor(logger: Logger, options: {
12
- readonly handleSignals: boolean;
13
- readonly handleFatalErrors: boolean;
14
- });
40
+ private registered;
41
+ private forcedExitTimer;
42
+ constructor(logger: Logger, options: SignalHandlerOptions);
15
43
  /**
16
44
  * Registers signal handlers.
45
+ *
46
+ * Registering twice is a no-op rather than a second set of listeners,
47
+ * so a stop/start cycle cannot accumulate handlers.
17
48
  */
18
- register(shutdownHandler: () => void): void;
49
+ register(shutdownHandler: () => void | Promise<void>): void;
19
50
  /**
20
51
  * Removes all signal handlers.
52
+ *
53
+ * Safe to call when nothing is registered, so it can run on both the
54
+ * success and failure paths of a shutdown.
21
55
  */
22
56
  unregister(): void;
23
57
  /**
24
- * Handles SIGTERM signal.
58
+ * Whether a shutdown has been initiated by a signal.
25
59
  */
60
+ get shuttingDown(): boolean;
26
61
  private handleTermination;
27
- /**
28
- * Handles SIGINT signal.
29
- */
30
62
  private handleInterruption;
31
63
  /**
32
64
  * Handles uncaught exceptions.
65
+ *
66
+ * The process state is no longer trustworthy after one, so this shuts
67
+ * down and then exits non-zero rather than continuing to serve.
33
68
  */
34
69
  private handleUncaughtException;
70
+ private handleUnhandledRejection;
35
71
  /**
36
- * Handles unhandled promise rejections.
72
+ * Shuts down after a fatal error and exits non-zero.
37
73
  */
38
- private handleUnhandledRejection;
74
+ private initiateFatalShutdown;
39
75
  /**
40
76
  * Initiates graceful shutdown.
77
+ *
78
+ * A second termination signal exits immediately: an operator pressing
79
+ * Ctrl-C again on a stuck shutdown is asking for exactly that.
41
80
  */
42
81
  private initiateShutdown;
82
+ private exit;
43
83
  }
44
84
  //# sourceMappingURL=signalHandler.core.d.ts.map