@zudojs/runtime 0.1.0 → 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.
- package/LICENSE +21 -0
- package/README.md +127 -6
- package/dist/dependencyGraph/dependencyGraph.core.d.ts +11 -5
- package/dist/dependencyGraph/dependencyGraph.core.js +13 -12
- package/dist/dependencyGraph/dependencyGraph.type.d.ts +0 -7
- package/dist/dependencyGraph/index.d.ts +2 -2
- package/dist/dependencyGraph/index.js +1 -1
- package/dist/health/health.core.d.ts +12 -0
- package/dist/health/health.core.js +55 -0
- package/dist/health/index.d.ts +5 -0
- package/dist/health/index.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/index.d.ts +1 -1
- package/dist/lifecycle/lifecycle.core.d.ts +41 -10
- package/dist/lifecycle/lifecycle.core.js +238 -61
- package/dist/lifecycle/lifecycle.type.d.ts +48 -1
- package/dist/readiness/index.d.ts +1 -1
- package/dist/readiness/readiness.core.d.ts +36 -4
- package/dist/readiness/readiness.core.js +109 -6
- package/dist/readiness/readiness.type.d.ts +15 -1
- package/dist/registry/registry.core.d.ts +14 -1
- package/dist/registry/registry.core.js +41 -2
- package/dist/runtime/runtime.core.d.ts +75 -5
- package/dist/runtime/runtime.core.js +199 -43
- package/dist/runtimeContext/index.d.ts +2 -2
- package/dist/runtimeContext/index.js +1 -1
- package/dist/runtimeContext/runtimeContext.core.d.ts +13 -4
- package/dist/runtimeContext/runtimeContext.core.js +22 -11
- package/dist/runtimeContext/runtimeContext.type.d.ts +12 -13
- package/dist/runtimeError/runtimeError.base.js +1 -1
- package/dist/runtimeEvents/index.d.ts +2 -2
- package/dist/runtimeEvents/index.js +1 -1
- package/dist/runtimeEvents/runtimeEvents.core.d.ts +15 -1
- package/dist/runtimeEvents/runtimeEvents.core.js +26 -0
- package/dist/runtimeEvents/runtimeEvents.type.d.ts +19 -4
- package/dist/runtimeOptions/runtimeOptions.core.js +8 -2
- package/dist/runtimeOptions/runtimeOptions.type.d.ts +29 -1
- package/dist/runtimeOptions/runtimeOptions.type.js +6 -0
- package/dist/runtimeState/index.d.ts +1 -1
- package/dist/runtimeState/runtimeState.core.d.ts +4 -0
- package/dist/runtimeState/runtimeState.core.js +17 -4
- package/dist/runtimeState/runtimeState.type.d.ts +14 -17
- package/dist/shutdown/shutdown.core.d.ts +20 -1
- package/dist/shutdown/shutdown.core.js +55 -29
- package/dist/signalHandler/signalHandler.core.d.ts +53 -13
- package/dist/signalHandler/signalHandler.core.js +97 -27
- package/dist/startup/startup.core.d.ts +3 -2
- package/dist/startup/startup.core.js +52 -46
- package/dist/testRuntime/index.d.ts +6 -1
- package/dist/testRuntime/index.js +5 -1
- package/dist/testRuntime/testRuntime.core.d.ts +21 -2
- package/dist/testRuntime/testRuntime.core.js +23 -6
- package/package.json +29 -22
- package/dist/.tsbuildinfo +0 -1
- package/dist/dependencyGraph/dependencyGraph.core.d.ts.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.core.js.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.type.d.ts.map +0 -1
- package/dist/dependencyGraph/dependencyGraph.type.js.map +0 -1
- package/dist/dependencyGraph/index.d.ts.map +0 -1
- package/dist/dependencyGraph/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle/index.d.ts.map +0 -1
- package/dist/lifecycle/index.js.map +0 -1
- package/dist/lifecycle/lifecycle.core.d.ts.map +0 -1
- package/dist/lifecycle/lifecycle.core.js.map +0 -1
- package/dist/lifecycle/lifecycle.type.d.ts.map +0 -1
- package/dist/lifecycle/lifecycle.type.js.map +0 -1
- package/dist/readiness/index.d.ts.map +0 -1
- package/dist/readiness/index.js.map +0 -1
- package/dist/readiness/readiness.core.d.ts.map +0 -1
- package/dist/readiness/readiness.core.js.map +0 -1
- package/dist/readiness/readiness.type.d.ts.map +0 -1
- package/dist/readiness/readiness.type.js.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/registry/registry.core.d.ts.map +0 -1
- package/dist/registry/registry.core.js.map +0 -1
- package/dist/runtime/runtime.core.d.ts.map +0 -1
- package/dist/runtime/runtime.core.js.map +0 -1
- package/dist/runtimeContext/index.d.ts.map +0 -1
- package/dist/runtimeContext/index.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.core.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.core.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.factory.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.factory.js.map +0 -1
- package/dist/runtimeContext/runtimeContext.type.d.ts.map +0 -1
- package/dist/runtimeContext/runtimeContext.type.js.map +0 -1
- package/dist/runtimeError/index.d.ts.map +0 -1
- package/dist/runtimeError/index.js.map +0 -1
- package/dist/runtimeError/runtimeError.base.d.ts.map +0 -1
- package/dist/runtimeError/runtimeError.base.js.map +0 -1
- package/dist/runtimeEvents/index.d.ts.map +0 -1
- package/dist/runtimeEvents/index.js.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.core.d.ts.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.core.js.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.type.d.ts.map +0 -1
- package/dist/runtimeEvents/runtimeEvents.type.js.map +0 -1
- package/dist/runtimeOptions/index.d.ts.map +0 -1
- package/dist/runtimeOptions/index.js.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.core.d.ts.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.core.js.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.type.d.ts.map +0 -1
- package/dist/runtimeOptions/runtimeOptions.type.js.map +0 -1
- package/dist/runtimeState/index.d.ts.map +0 -1
- package/dist/runtimeState/index.js.map +0 -1
- package/dist/runtimeState/runtimeState.core.d.ts.map +0 -1
- package/dist/runtimeState/runtimeState.core.js.map +0 -1
- package/dist/runtimeState/runtimeState.type.d.ts.map +0 -1
- package/dist/runtimeState/runtimeState.type.js.map +0 -1
- package/dist/shutdown/index.d.ts.map +0 -1
- package/dist/shutdown/index.js.map +0 -1
- package/dist/shutdown/shutdown.core.d.ts.map +0 -1
- package/dist/shutdown/shutdown.core.js.map +0 -1
- package/dist/signalHandler/index.d.ts.map +0 -1
- package/dist/signalHandler/index.js.map +0 -1
- package/dist/signalHandler/signalHandler.core.d.ts.map +0 -1
- package/dist/signalHandler/signalHandler.core.js.map +0 -1
- package/dist/startup/index.d.ts.map +0 -1
- package/dist/startup/index.js.map +0 -1
- package/dist/startup/startup.core.d.ts.map +0 -1
- package/dist/startup/startup.core.js.map +0 -1
- package/dist/testRuntime/index.d.ts.map +0 -1
- package/dist/testRuntime/index.js.map +0 -1
- package/dist/testRuntime/testRuntime.core.d.ts.map +0 -1
- package/dist/testRuntime/testRuntime.core.js.map +0 -1
|
@@ -1,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.
|
|
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,12 +1,15 @@
|
|
|
1
1
|
import { DEFAULT_RUNTIME_OPTIONS } from "./runtimeOptions.type.js";
|
|
2
|
-
import { createRuntimeId } from "../runtimeContext/runtimeContext.factory.js";
|
|
3
2
|
/**
|
|
4
3
|
* Resolves runtime options with defaults applied.
|
|
5
4
|
*/
|
|
6
5
|
export function resolveRuntimeOptions(options) {
|
|
6
|
+
// Spreading `options` wholesale lets an explicitly-undefined key erase
|
|
7
|
+
// its default — a common footgun when callers build options with
|
|
8
|
+
// optional fields. Only defined values override.
|
|
9
|
+
const provided = Object.fromEntries(Object.entries(options ?? {}).filter(([, value]) => value !== undefined));
|
|
7
10
|
return Object.freeze({
|
|
8
11
|
...DEFAULT_RUNTIME_OPTIONS,
|
|
9
|
-
...
|
|
12
|
+
...provided,
|
|
10
13
|
});
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
@@ -25,6 +28,9 @@ export function validateRuntimeOptions(options) {
|
|
|
25
28
|
if (options.startupTimeout <= 0) {
|
|
26
29
|
throw new Error("Startup timeout must be positive.");
|
|
27
30
|
}
|
|
31
|
+
if (options.readinessCheckTimeout < 0) {
|
|
32
|
+
throw new Error(`Readiness check timeout must be zero or positive, got ${options.readinessCheckTimeout}. Use 0 to run checks without a bound.`);
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* 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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
39
|
+
* A module that failed during shutdown.
|
|
36
40
|
*/
|
|
37
|
-
export interface
|
|
38
|
-
readonly
|
|
39
|
-
readonly
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
8
|
-
private readonly handleFatalErrors;
|
|
37
|
+
private readonly options;
|
|
9
38
|
private shutdownHandler;
|
|
10
39
|
private isShuttingDown;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
72
|
+
* Shuts down after a fatal error and exits non-zero.
|
|
37
73
|
*/
|
|
38
|
-
private
|
|
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
|