@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.
- 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 +122 -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 +210 -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.d.ts +5 -0
- package/dist/runtimeOptions/runtimeOptions.core.js +14 -1
- 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 +33 -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,90 +1,160 @@
|
|
|
1
|
+
/** Default grace period for a fatal-error shutdown. */
|
|
2
|
+
const DEFAULT_FATAL_EXIT_TIMEOUT = 10_000;
|
|
1
3
|
/**
|
|
2
4
|
* Signal handler for process lifecycle events.
|
|
3
5
|
*/
|
|
4
6
|
export class SignalHandler {
|
|
5
7
|
logger;
|
|
6
|
-
|
|
7
|
-
handleFatalErrors;
|
|
8
|
+
options;
|
|
8
9
|
shutdownHandler = null;
|
|
9
10
|
isShuttingDown = false;
|
|
11
|
+
registered = false;
|
|
12
|
+
forcedExitTimer = null;
|
|
10
13
|
constructor(logger, options) {
|
|
11
14
|
this.logger = logger;
|
|
12
|
-
this.
|
|
13
|
-
this.handleFatalErrors = options.handleFatalErrors;
|
|
15
|
+
this.options = options;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Registers signal handlers.
|
|
19
|
+
*
|
|
20
|
+
* Registering twice is a no-op rather than a second set of listeners,
|
|
21
|
+
* so a stop/start cycle cannot accumulate handlers.
|
|
17
22
|
*/
|
|
18
23
|
register(shutdownHandler) {
|
|
19
24
|
this.shutdownHandler = shutdownHandler;
|
|
20
|
-
if (this.
|
|
25
|
+
if (this.registered) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.registered = true;
|
|
29
|
+
this.isShuttingDown = false;
|
|
30
|
+
if (this.options.handleSignals) {
|
|
21
31
|
process.on("SIGTERM", this.handleTermination);
|
|
22
32
|
process.on("SIGINT", this.handleInterruption);
|
|
23
33
|
}
|
|
24
|
-
if (this.handleFatalErrors) {
|
|
34
|
+
if (this.options.handleFatalErrors) {
|
|
25
35
|
process.on("uncaughtException", this.handleUncaughtException);
|
|
26
36
|
process.on("unhandledRejection", this.handleUnhandledRejection);
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* Removes all signal handlers.
|
|
41
|
+
*
|
|
42
|
+
* Safe to call when nothing is registered, so it can run on both the
|
|
43
|
+
* success and failure paths of a shutdown.
|
|
31
44
|
*/
|
|
32
45
|
unregister() {
|
|
33
|
-
if (this.
|
|
46
|
+
if (this.forcedExitTimer) {
|
|
47
|
+
clearTimeout(this.forcedExitTimer);
|
|
48
|
+
this.forcedExitTimer = null;
|
|
49
|
+
}
|
|
50
|
+
if (!this.registered) {
|
|
51
|
+
this.shutdownHandler = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (this.options.handleSignals) {
|
|
34
55
|
process.off("SIGTERM", this.handleTermination);
|
|
35
56
|
process.off("SIGINT", this.handleInterruption);
|
|
36
57
|
}
|
|
37
|
-
if (this.handleFatalErrors) {
|
|
58
|
+
if (this.options.handleFatalErrors) {
|
|
38
59
|
process.off("uncaughtException", this.handleUncaughtException);
|
|
39
60
|
process.off("unhandledRejection", this.handleUnhandledRejection);
|
|
40
61
|
}
|
|
62
|
+
this.registered = false;
|
|
63
|
+
this.isShuttingDown = false;
|
|
41
64
|
this.shutdownHandler = null;
|
|
42
65
|
}
|
|
43
66
|
/**
|
|
44
|
-
*
|
|
67
|
+
* Whether a shutdown has been initiated by a signal.
|
|
45
68
|
*/
|
|
69
|
+
get shuttingDown() {
|
|
70
|
+
return this.isShuttingDown;
|
|
71
|
+
}
|
|
46
72
|
handleTermination = () => {
|
|
47
73
|
this.logger.info("Received SIGTERM signal.");
|
|
48
|
-
this.initiateShutdown();
|
|
74
|
+
this.initiateShutdown("SIGTERM");
|
|
49
75
|
};
|
|
50
|
-
/**
|
|
51
|
-
* Handles SIGINT signal.
|
|
52
|
-
*/
|
|
53
76
|
handleInterruption = () => {
|
|
54
77
|
this.logger.info("Received SIGINT signal.");
|
|
55
|
-
this.initiateShutdown();
|
|
78
|
+
this.initiateShutdown("SIGINT");
|
|
56
79
|
};
|
|
57
80
|
/**
|
|
58
81
|
* Handles uncaught exceptions.
|
|
82
|
+
*
|
|
83
|
+
* The process state is no longer trustworthy after one, so this shuts
|
|
84
|
+
* down and then exits non-zero rather than continuing to serve.
|
|
59
85
|
*/
|
|
60
86
|
handleUncaughtException = (error) => {
|
|
61
|
-
this.logger.error("Uncaught exception.", {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
87
|
+
this.logger.error("Uncaught exception.", {
|
|
88
|
+
errorMessage: error.message,
|
|
89
|
+
stack: error.stack,
|
|
90
|
+
});
|
|
91
|
+
this.initiateFatalShutdown();
|
|
65
92
|
};
|
|
66
|
-
/**
|
|
67
|
-
* Handles unhandled promise rejections.
|
|
68
|
-
*/
|
|
69
93
|
handleUnhandledRejection = (reason) => {
|
|
70
94
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
71
|
-
this.logger.error("Unhandled rejection.", {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
this.logger.error("Unhandled rejection.", {
|
|
96
|
+
reason: message,
|
|
97
|
+
...(reason instanceof Error && reason.stack
|
|
98
|
+
? { stack: reason.stack }
|
|
99
|
+
: {}),
|
|
100
|
+
});
|
|
101
|
+
this.initiateFatalShutdown();
|
|
75
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* Shuts down after a fatal error and exits non-zero.
|
|
105
|
+
*/
|
|
106
|
+
initiateFatalShutdown() {
|
|
107
|
+
if (this.options.exitOnFatalError === false) {
|
|
108
|
+
this.initiateShutdown("fatal");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const timeout = this.options.fatalExitTimeout ?? DEFAULT_FATAL_EXIT_TIMEOUT;
|
|
112
|
+
if (!this.isShuttingDown) {
|
|
113
|
+
// Exit even if the shutdown itself hangs — a process that is
|
|
114
|
+
// already in an undefined state must not linger indefinitely.
|
|
115
|
+
this.forcedExitTimer = setTimeout(() => {
|
|
116
|
+
this.logger.error("Fatal shutdown timed out; exiting.");
|
|
117
|
+
this.exit(1);
|
|
118
|
+
}, timeout);
|
|
119
|
+
this.forcedExitTimer.unref?.();
|
|
120
|
+
}
|
|
121
|
+
void Promise.resolve(this.initiateShutdown("fatal")).finally(() => {
|
|
122
|
+
if (this.forcedExitTimer) {
|
|
123
|
+
clearTimeout(this.forcedExitTimer);
|
|
124
|
+
this.forcedExitTimer = null;
|
|
125
|
+
}
|
|
126
|
+
this.exit(1);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
76
129
|
/**
|
|
77
130
|
* Initiates graceful shutdown.
|
|
131
|
+
*
|
|
132
|
+
* A second termination signal exits immediately: an operator pressing
|
|
133
|
+
* Ctrl-C again on a stuck shutdown is asking for exactly that.
|
|
78
134
|
*/
|
|
79
|
-
initiateShutdown() {
|
|
135
|
+
initiateShutdown(source) {
|
|
80
136
|
if (this.isShuttingDown) {
|
|
137
|
+
if ((this.options.forceExitOnSecondSignal ?? true) &&
|
|
138
|
+
source !== "fatal") {
|
|
139
|
+
this.logger.warn(`Received a second ${source} while shutting down; exiting immediately.`);
|
|
140
|
+
this.exit(1);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
81
143
|
this.logger.warn("Shutdown already in progress, ignoring signal.");
|
|
82
144
|
return;
|
|
83
145
|
}
|
|
84
146
|
this.isShuttingDown = true;
|
|
85
147
|
if (this.shutdownHandler) {
|
|
86
|
-
this.shutdownHandler()
|
|
148
|
+
return Promise.resolve(this.shutdownHandler()).catch((error) => {
|
|
149
|
+
this.logger.error("Shutdown handler failed.", {
|
|
150
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
87
153
|
}
|
|
88
154
|
}
|
|
155
|
+
exit(code) {
|
|
156
|
+
const exit = this.options.exit ?? ((value) => process.exit(value));
|
|
157
|
+
exit(code);
|
|
158
|
+
}
|
|
89
159
|
}
|
|
90
160
|
//# sourceMappingURL=signalHandler.core.js.map
|
|
@@ -1,12 +1,13 @@
|
|
|
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";
|
|
4
5
|
/**
|
|
5
6
|
* Executes the startup sequence.
|
|
6
7
|
*/
|
|
7
|
-
export declare function executeStartup(lifecycle: LifecycleManager, runtimeId: string, eventBus: EventBus | undefined, logger: Logger, emitEvents: boolean): Promise<void>;
|
|
8
|
+
export declare function executeStartup(lifecycle: LifecycleManager, runtimeId: string, eventBus: EventBus | undefined, logger: Logger, emitEvents: boolean, startupTimeout?: number): Promise<void>;
|
|
8
9
|
/**
|
|
9
10
|
* Rolls back a failed startup.
|
|
10
11
|
*/
|
|
11
|
-
export declare function rollbackStartup(lifecycle: LifecycleManager, logger: Logger): Promise<
|
|
12
|
+
export declare function rollbackStartup(lifecycle: LifecycleManager, logger: Logger): Promise<readonly LifecycleFailure[]>;
|
|
12
13
|
//# sourceMappingURL=startup.core.d.ts.map
|
|
@@ -1,35 +1,56 @@
|
|
|
1
1
|
import { createEvent } from "@zudojs/events";
|
|
2
|
+
import { createFailureEventPayload, publishRuntimeEvent, } from "../runtimeEvents/index.js";
|
|
2
3
|
import { LifecycleManager } from "../lifecycle/index.js";
|
|
3
|
-
import { RuntimeStartError } from "../runtimeError/index.js";
|
|
4
|
+
import { RuntimeStartError, 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
|
+
* Runs the startup sequence under a bound.
|
|
9
|
+
*
|
|
10
|
+
* `startupTimeout` was validated as positive and then never enforced, so
|
|
11
|
+
* a module whose `onInitialize` never settles hung the boot forever with
|
|
12
|
+
* no diagnostic. The timer is cleared whichever side wins.
|
|
6
13
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
async function withStartupTimeout(operation, timeoutMs) {
|
|
15
|
+
if (timeoutMs <= 0) {
|
|
16
|
+
return operation;
|
|
17
|
+
}
|
|
18
|
+
// The startup promise keeps running if the timeout wins; attach a
|
|
19
|
+
// handler now so its eventual rejection is never unhandled.
|
|
20
|
+
operation.catch(() => { });
|
|
21
|
+
let timer;
|
|
22
|
+
const timeout = new Promise((_, reject) => {
|
|
23
|
+
timer = setTimeout(() => reject(new RuntimeTimeoutError("startup", timeoutMs)), Math.min(timeoutMs, MAX_TIMER_DELAY));
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
return await Promise.race([operation, timeout]);
|
|
18
28
|
}
|
|
29
|
+
finally {
|
|
30
|
+
if (timer !== undefined) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Executes the startup sequence.
|
|
37
|
+
*/
|
|
38
|
+
export async function executeStartup(lifecycle, runtimeId, eventBus, logger, emitEvents, startupTimeout = 0) {
|
|
39
|
+
return withStartupTimeout(runStartup(lifecycle, runtimeId, eventBus, logger, emitEvents), startupTimeout);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Runs the startup sequence.
|
|
43
|
+
*/
|
|
44
|
+
async function runStartup(lifecycle, runtimeId, eventBus, logger, emitEvents) {
|
|
45
|
+
// Per-module `runtime.module.*` events are emitted by the lifecycle
|
|
46
|
+
// manager, which is the only layer that knows which module is running.
|
|
19
47
|
const initResult = await lifecycle.initialize();
|
|
20
48
|
if (initResult.failed.length > 0) {
|
|
21
49
|
const failure = initResult.failed[0];
|
|
22
50
|
if (emitEvents && eventBus) {
|
|
23
|
-
eventBus
|
|
24
|
-
type: "runtime.
|
|
25
|
-
payload:
|
|
26
|
-
runtimeId,
|
|
27
|
-
state: "initialization_failed",
|
|
28
|
-
timestamp: new Date(),
|
|
29
|
-
error: failure.error,
|
|
30
|
-
phase: "initialize",
|
|
31
|
-
failedModuleId: failure.moduleId,
|
|
32
|
-
},
|
|
51
|
+
publishRuntimeEvent(eventBus, logger, createEvent({
|
|
52
|
+
type: "runtime.failed",
|
|
53
|
+
payload: createFailureEventPayload(runtimeId, "initialization_failed", failure.error, "initialize", failure.moduleId),
|
|
33
54
|
}));
|
|
34
55
|
}
|
|
35
56
|
throw new RuntimeStartError(`Module "${failure.moduleId}" failed during initialization.`, {
|
|
@@ -42,31 +63,13 @@ export async function executeStartup(lifecycle, runtimeId, eventBus, logger, emi
|
|
|
42
63
|
modules: initResult.succeeded,
|
|
43
64
|
durationMs: initResult.durationMs,
|
|
44
65
|
});
|
|
45
|
-
// Start modules
|
|
46
|
-
if (emitEvents && eventBus) {
|
|
47
|
-
eventBus.publish(createEvent({
|
|
48
|
-
type: "runtime.module.starting",
|
|
49
|
-
payload: {
|
|
50
|
-
runtimeId,
|
|
51
|
-
timestamp: new Date(),
|
|
52
|
-
state: "starting",
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
66
|
const startResult = await lifecycle.start();
|
|
57
67
|
if (startResult.failed.length > 0) {
|
|
58
68
|
const failure = startResult.failed[0];
|
|
59
69
|
if (emitEvents && eventBus) {
|
|
60
|
-
eventBus
|
|
61
|
-
type: "runtime.
|
|
62
|
-
payload:
|
|
63
|
-
runtimeId,
|
|
64
|
-
state: "startup_failed",
|
|
65
|
-
timestamp: new Date(),
|
|
66
|
-
error: failure.error,
|
|
67
|
-
phase: "start",
|
|
68
|
-
failedModuleId: failure.moduleId,
|
|
69
|
-
},
|
|
70
|
+
publishRuntimeEvent(eventBus, logger, createEvent({
|
|
71
|
+
type: "runtime.failed",
|
|
72
|
+
payload: createFailureEventPayload(runtimeId, "startup_failed", failure.error, "start", failure.moduleId),
|
|
70
73
|
}));
|
|
71
74
|
}
|
|
72
75
|
throw new RuntimeStartError(`Module "${failure.moduleId}" failed during startup.`, {
|
|
@@ -85,7 +88,10 @@ export async function executeStartup(lifecycle, runtimeId, eventBus, logger, emi
|
|
|
85
88
|
*/
|
|
86
89
|
export async function rollbackStartup(lifecycle, logger) {
|
|
87
90
|
logger.info("Rolling back module startup.");
|
|
88
|
-
await lifecycle.rollback();
|
|
89
|
-
logger.info("Module rollback complete."
|
|
91
|
+
const failures = await lifecycle.rollback();
|
|
92
|
+
logger.info("Module rollback complete.", {
|
|
93
|
+
failedModules: failures.map((failure) => failure.moduleId),
|
|
94
|
+
});
|
|
95
|
+
return failures;
|
|
90
96
|
}
|
|
91
97
|
//# sourceMappingURL=startup.core.js.map
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zudojs/runtime
|
|
2
|
+
* @zudojs/runtime/testing
|
|
3
|
+
*
|
|
4
|
+
* Testing helpers. Exposed through the "@zudojs/runtime/testing" subpath
|
|
5
|
+
* rather than the package root, so a test-only module never sits on the
|
|
6
|
+
* main entry point.
|
|
3
7
|
*/
|
|
8
|
+
export type { MockModule } from "./testRuntime.core.js";
|
|
4
9
|
export { createTestRuntime, createMockModule, withTestRuntime, } from "./testRuntime.core.js";
|
|
5
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zudojs/runtime
|
|
2
|
+
* @zudojs/runtime/testing
|
|
3
|
+
*
|
|
4
|
+
* Testing helpers. Exposed through the "@zudojs/runtime/testing" subpath
|
|
5
|
+
* rather than the package root, so a test-only module never sits on the
|
|
6
|
+
* main entry point.
|
|
3
7
|
*/
|
|
4
8
|
export { createTestRuntime, createMockModule, withTestRuntime, } from "./testRuntime.core.js";
|
|
5
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -15,14 +15,33 @@ import type { RuntimeOptions } from "../runtimeOptions/runtimeOptions.type.js";
|
|
|
15
15
|
* @returns A runtime instance ready for testing.
|
|
16
16
|
*/
|
|
17
17
|
export declare function createTestRuntime(modules?: Module[], options?: Partial<RuntimeOptions>): DefaultRuntime;
|
|
18
|
+
/**
|
|
19
|
+
* A module whose lifecycle hooks record their calls.
|
|
20
|
+
*/
|
|
21
|
+
export interface MockModule extends Module {
|
|
22
|
+
/** How many times each hook ran, in call order. */
|
|
23
|
+
readonly calls: {
|
|
24
|
+
readonly onInitialize: number;
|
|
25
|
+
readonly onReady: number;
|
|
26
|
+
readonly onShutdown: number;
|
|
27
|
+
readonly onDestroy: number;
|
|
28
|
+
};
|
|
29
|
+
/** Hook names in the order they were invoked. */
|
|
30
|
+
readonly callOrder: readonly string[];
|
|
31
|
+
}
|
|
18
32
|
/**
|
|
19
33
|
* Creates a mock module for testing.
|
|
20
34
|
*
|
|
35
|
+
* The hooks are plain counting functions rather than `vi.fn()`: this
|
|
36
|
+
* module ships in the package, so it must not import a test runner —
|
|
37
|
+
* `vitest` is a dev dependency consumers do not install, and `require`
|
|
38
|
+
* is not available in an ESM package at all.
|
|
39
|
+
*
|
|
21
40
|
* @param id - Module identifier.
|
|
22
41
|
* @param dependencies - Module dependencies.
|
|
23
|
-
* @returns A mock module
|
|
42
|
+
* @returns A mock module that records its lifecycle calls.
|
|
24
43
|
*/
|
|
25
|
-
export declare function createMockModule(id: string, dependencies?: string[]):
|
|
44
|
+
export declare function createMockModule(id: string, dependencies?: string[]): MockModule;
|
|
26
45
|
/**
|
|
27
46
|
* Starts a test runtime, runs a callback, and stops it.
|
|
28
47
|
*
|
|
@@ -45,20 +45,37 @@ export function createTestRuntime(modules = [], options = {}) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Creates a mock module for testing.
|
|
47
47
|
*
|
|
48
|
+
* The hooks are plain counting functions rather than `vi.fn()`: this
|
|
49
|
+
* module ships in the package, so it must not import a test runner —
|
|
50
|
+
* `vitest` is a dev dependency consumers do not install, and `require`
|
|
51
|
+
* is not available in an ESM package at all.
|
|
52
|
+
*
|
|
48
53
|
* @param id - Module identifier.
|
|
49
54
|
* @param dependencies - Module dependencies.
|
|
50
|
-
* @returns A mock module
|
|
55
|
+
* @returns A mock module that records its lifecycle calls.
|
|
51
56
|
*/
|
|
52
57
|
export function createMockModule(id, dependencies = []) {
|
|
53
|
-
const
|
|
58
|
+
const calls = {
|
|
59
|
+
onInitialize: 0,
|
|
60
|
+
onReady: 0,
|
|
61
|
+
onShutdown: 0,
|
|
62
|
+
onDestroy: 0,
|
|
63
|
+
};
|
|
64
|
+
const callOrder = [];
|
|
65
|
+
const record = (hook) => async () => {
|
|
66
|
+
calls[hook] += 1;
|
|
67
|
+
callOrder.push(hook);
|
|
68
|
+
};
|
|
54
69
|
return {
|
|
55
70
|
id,
|
|
56
71
|
name: `Module ${id}`,
|
|
57
72
|
dependencies,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
calls,
|
|
74
|
+
callOrder,
|
|
75
|
+
onInitialize: record("onInitialize"),
|
|
76
|
+
onReady: record("onReady"),
|
|
77
|
+
onShutdown: record("onShutdown"),
|
|
78
|
+
onDestroy: record("onDestroy"),
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/runtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Application lifecycle orchestrator with dependency ordering, rollback, signals, and readiness checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,36 +10,36 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./
|
|
14
|
-
"types": "./dist/
|
|
15
|
-
"import": "./dist/
|
|
13
|
+
"./testing": {
|
|
14
|
+
"types": "./dist/testRuntime/index.d.ts",
|
|
15
|
+
"import": "./dist/testRuntime/index.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"!dist/**/*.map",
|
|
21
|
+
"!dist/**/*.tsbuildinfo",
|
|
22
|
+
"!dist/.tsbuildinfo"
|
|
20
23
|
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc -p tsconfig.json",
|
|
23
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
-
"test": "vitest run",
|
|
25
|
-
"test:watch": "vitest",
|
|
26
|
-
"clean": "tsc -p tsconfig.json --clean"
|
|
27
|
-
},
|
|
28
24
|
"devDependencies": {
|
|
29
25
|
"@types/node": "^26.4.1",
|
|
30
|
-
"typescript": "
|
|
26
|
+
"typescript": "7.0.2",
|
|
31
27
|
"vitest": "^4.1.11"
|
|
32
28
|
},
|
|
33
29
|
"dependencies": {
|
|
34
|
-
"@zudojs/errors": "0.1
|
|
35
|
-
"@zudojs/constants": "0.1
|
|
36
|
-
"@zudojs/container": "
|
|
37
|
-
"@zudojs/config": "0.1
|
|
38
|
-
"@zudojs/logger": "
|
|
39
|
-
"@zudojs/events": "0.1
|
|
40
|
-
"@zudojs/core": "
|
|
30
|
+
"@zudojs/errors": "1.0.1",
|
|
31
|
+
"@zudojs/constants": "1.0.1",
|
|
32
|
+
"@zudojs/container": "1.1.0",
|
|
33
|
+
"@zudojs/config": "1.0.1",
|
|
34
|
+
"@zudojs/logger": "1.1.0",
|
|
35
|
+
"@zudojs/events": "1.0.1",
|
|
36
|
+
"@zudojs/core": "1.1.0"
|
|
41
37
|
},
|
|
42
38
|
"license": "MIT",
|
|
39
|
+
"author": {
|
|
40
|
+
"name": "Oluwayemi Oyinlola",
|
|
41
|
+
"url": "https://github.com/oyinlola-tech"
|
|
42
|
+
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
@@ -54,8 +54,19 @@
|
|
|
54
54
|
"services"
|
|
55
55
|
],
|
|
56
56
|
"homepage": "https://github.com/oyinlola-tech/zudo#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
59
|
+
},
|
|
57
60
|
"repository": {
|
|
58
61
|
"type": "git",
|
|
59
|
-
"url": "https://github.com/oyinlola-tech/zudo"
|
|
62
|
+
"url": "https://github.com/oyinlola-tech/zudo",
|
|
63
|
+
"directory": "packages/runtime"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc -p tsconfig.json",
|
|
67
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"clean": "rm -rf dist"
|
|
60
71
|
}
|
|
61
|
-
}
|
|
72
|
+
}
|