@zudojs/lifecycle 1.0.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/README.md +13 -1
- package/dist/lifecycleInternal/asyncUtils.core.js +13 -1
- package/dist/lifecycleManager/lifecycleManager.context.d.ts +33 -0
- package/dist/lifecycleManager/lifecycleManager.context.js +20 -0
- package/dist/lifecycleManager/lifecycleManager.core.js +1 -0
- package/dist/lifecycleManager/lifecycleManager.shutdown.js +43 -6
- package/dist/lifecycleManager/lifecycleManager.startup.js +89 -7
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -48,7 +48,19 @@ If a **critical** component (the default) fails any startup phase,
|
|
|
48
48
|
`start()` rolls the application back (`stop` → `dispose`) and then
|
|
49
49
|
**rejects** with a `LifecycleStartError`. Register a component with
|
|
50
50
|
`{ critical: false }` when its failure should not abort startup — the
|
|
51
|
-
component is marked `FAILED` and startup continues.
|
|
51
|
+
component is marked `FAILED` and startup continues. A failed component
|
|
52
|
+
takes no further part in startup (its later hooks are not invoked), and
|
|
53
|
+
components that `dependsOn` it are not started either: they are marked
|
|
54
|
+
`FAILED` with a `LifecycleComponentError` naming the failed dependency,
|
|
55
|
+
and their own `critical` flag decides whether startup aborts.
|
|
56
|
+
|
|
57
|
+
Rollback only undoes phases that ran: `stop()` is called on components
|
|
58
|
+
whose `start` phase ran, and `dispose()` on components whose
|
|
59
|
+
`initialize` phase ran.
|
|
60
|
+
|
|
61
|
+
Calling `shutdown()` while `start()` is in flight waits for the
|
|
62
|
+
executing stage to settle, tears down, and makes `start()` reject with a
|
|
63
|
+
`LifecycleStartError` — later stages are never launched.
|
|
52
64
|
|
|
53
65
|
## Shutdown
|
|
54
66
|
|
|
@@ -47,7 +47,19 @@ export async function withAbort(fn, signal) {
|
|
|
47
47
|
reject(new Error("Operation aborted"));
|
|
48
48
|
};
|
|
49
49
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
50
|
-
fn
|
|
50
|
+
// A synchronously throwing `fn` rejected via the Promise executor
|
|
51
|
+
// but skipped both `.then` branches, so its abort listener was
|
|
52
|
+
// never removed and accumulated on a long-lived signal.
|
|
53
|
+
let operation;
|
|
54
|
+
try {
|
|
55
|
+
operation = fn(signal);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
signal.removeEventListener("abort", onAbort);
|
|
59
|
+
reject(error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
operation
|
|
51
63
|
.then((result) => {
|
|
52
64
|
signal.removeEventListener("abort", onAbort);
|
|
53
65
|
resolve(result);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* plus helper functions for component state transitions and event emission.
|
|
6
6
|
*/
|
|
7
7
|
import { LifecycleState } from "@zudojs/constants";
|
|
8
|
+
import type { LifecyclePhase } from "@zudojs/constants";
|
|
8
9
|
import type { LifecycleStateMachine } from "../lifecycleState/lifecycleState.machine.js";
|
|
9
10
|
import type { LifecycleRegistry } from "../lifecycleRegistry/lifecycleRegistry.core.js";
|
|
10
11
|
import type { LifecycleExecutor } from "../lifecycleExecutor/lifecycleExecutor.core.js";
|
|
@@ -20,6 +21,16 @@ export interface LifecycleManagerContext {
|
|
|
20
21
|
readonly shutdownTimeout: number;
|
|
21
22
|
readonly componentStates: Map<string, LifecycleStateMachine>;
|
|
22
23
|
readonly results: Map<string, ExecutionResult[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Startup phases that were actually run for each component.
|
|
26
|
+
*
|
|
27
|
+
* Shutdown consults this so `stop()` is only invoked on components
|
|
28
|
+
* whose `start` phase ran and `dispose()` only on components whose
|
|
29
|
+
* `initialize` phase ran. Rollback after an early failure used to
|
|
30
|
+
* call `stop()` on components that had never started, which for a
|
|
31
|
+
* real server throws and was then reported as a component failure.
|
|
32
|
+
*/
|
|
33
|
+
readonly attempted: Map<string, Set<LifecyclePhase>>;
|
|
23
34
|
startTime: number;
|
|
24
35
|
/**
|
|
25
36
|
* Cancellation source for the current run.
|
|
@@ -39,6 +50,16 @@ export interface LifecycleManagerContext {
|
|
|
39
50
|
* teardown.
|
|
40
51
|
*/
|
|
41
52
|
shutdownPromise?: Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* The startup stage currently executing, if any.
|
|
55
|
+
*
|
|
56
|
+
* A shutdown requested mid-startup waits for this to settle before
|
|
57
|
+
* running `stop()`, so a component is never stopped while its own
|
|
58
|
+
* `start()` is still in flight, and startup checks for a requested
|
|
59
|
+
* shutdown before launching each further stage so no hook runs
|
|
60
|
+
* after teardown has completed.
|
|
61
|
+
*/
|
|
62
|
+
inFlight?: Promise<unknown>;
|
|
42
63
|
}
|
|
43
64
|
/** Records an execution result against its component. */
|
|
44
65
|
export declare function recordResult(ctx: LifecycleManagerContext, result: ExecutionResult): void;
|
|
@@ -47,6 +68,18 @@ export declare function recordResult(ctx: LifecycleManagerContext, result: Execu
|
|
|
47
68
|
* No-op if the transition is not valid from the current state.
|
|
48
69
|
*/
|
|
49
70
|
export declare function transitionComponent(ctx: LifecycleManagerContext, id: string, targetState: LifecycleState): void;
|
|
71
|
+
/**
|
|
72
|
+
* Marks a component as FAILED regardless of its current state.
|
|
73
|
+
*
|
|
74
|
+
* Used for components that are skipped because a dependency failed:
|
|
75
|
+
* they have not entered the phase, so no validated transition leads to
|
|
76
|
+
* FAILED from their current (IDLE / INITIALIZED / STARTED) state.
|
|
77
|
+
*/
|
|
78
|
+
export declare function failComponent(ctx: LifecycleManagerContext, id: string): void;
|
|
79
|
+
/** Records that a startup phase ran for a component. */
|
|
80
|
+
export declare function markAttempted(ctx: LifecycleManagerContext, id: string, phase: LifecyclePhase): void;
|
|
81
|
+
/** Returns whether a startup phase ran for a component. */
|
|
82
|
+
export declare function wasAttempted(ctx: LifecycleManagerContext, id: string, phase: LifecyclePhase): boolean;
|
|
50
83
|
/**
|
|
51
84
|
* Transitions multiple component state machines to the same target state.
|
|
52
85
|
*/
|
|
@@ -20,6 +20,26 @@ export function transitionComponent(ctx, id, targetState) {
|
|
|
20
20
|
sm.transition(targetState);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Marks a component as FAILED regardless of its current state.
|
|
25
|
+
*
|
|
26
|
+
* Used for components that are skipped because a dependency failed:
|
|
27
|
+
* they have not entered the phase, so no validated transition leads to
|
|
28
|
+
* FAILED from their current (IDLE / INITIALIZED / STARTED) state.
|
|
29
|
+
*/
|
|
30
|
+
export function failComponent(ctx, id) {
|
|
31
|
+
ctx.componentStates.get(id)?.forceState(LifecycleState.FAILED);
|
|
32
|
+
}
|
|
33
|
+
/** Records that a startup phase ran for a component. */
|
|
34
|
+
export function markAttempted(ctx, id, phase) {
|
|
35
|
+
const phases = ctx.attempted.get(id) ?? new Set();
|
|
36
|
+
phases.add(phase);
|
|
37
|
+
ctx.attempted.set(id, phases);
|
|
38
|
+
}
|
|
39
|
+
/** Returns whether a startup phase ran for a component. */
|
|
40
|
+
export function wasAttempted(ctx, id, phase) {
|
|
41
|
+
return ctx.attempted.get(id)?.has(phase) ?? false;
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Transitions multiple component state machines to the same target state.
|
|
25
45
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { LifecyclePhase, LifecycleState } from "@zudojs/constants";
|
|
7
7
|
import { buildExecutionPlan } from "../lifecyclePlan/lifecyclePlan.core.js";
|
|
8
8
|
import { createLifecycleContext } from "../lifecycleContext/lifecycleContext.type.js";
|
|
9
|
-
import { emitComponentFailed, recordResult, transitionComponent, } from "./lifecycleManager.context.js";
|
|
9
|
+
import { emitComponentFailed, recordResult, transitionComponent, wasAttempted, } from "./lifecycleManager.context.js";
|
|
10
10
|
/** Shutdown phases in execution order. */
|
|
11
11
|
const SHUTDOWN_PHASES = [LifecyclePhase.STOP, LifecyclePhase.DISPOSE];
|
|
12
12
|
/**
|
|
@@ -38,6 +38,13 @@ async function runShutdown(ctx) {
|
|
|
38
38
|
}
|
|
39
39
|
ctx.events.emit("application:stopping", {});
|
|
40
40
|
const deadline = Date.now() + ctx.shutdownTimeout;
|
|
41
|
+
// A startup stage still executing must settle before its components
|
|
42
|
+
// are stopped, otherwise `stop()` overlaps the component's own
|
|
43
|
+
// `start()`. Startup itself refuses to launch further stages once
|
|
44
|
+
// `shutdownPromise` is set, so this wait is bounded by one stage.
|
|
45
|
+
if (ctx.inFlight !== undefined) {
|
|
46
|
+
await raceDeadline(ctx, ctx.inFlight.then(() => undefined, () => undefined), Math.max(deadline - Date.now(), 1));
|
|
47
|
+
}
|
|
41
48
|
// The shutdown deadline used to be checked only BETWEEN the two
|
|
42
49
|
// phases, so a single hook that never settled hung shutdown (and the
|
|
43
50
|
// process) forever. Race the whole phase against the remaining
|
|
@@ -91,11 +98,24 @@ async function raceDeadline(ctx, phase, remainingMs) {
|
|
|
91
98
|
// rejection once the race has been decided.
|
|
92
99
|
void phase.catch(() => { });
|
|
93
100
|
}
|
|
94
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Executes a single shutdown phase across all registered components.
|
|
103
|
+
*
|
|
104
|
+
* A hook only runs for components that reached the matching startup
|
|
105
|
+
* phase: `stop()` when `start` ran, `dispose()` when `initialize` ran.
|
|
106
|
+
* Rollback after an early failure — and `shutdown()` on a manager that
|
|
107
|
+
* was never started — used to call `stop()` on components that had
|
|
108
|
+
* never started; a real server's `close()` throws in that situation
|
|
109
|
+
* and the phantom failure was then recorded against the component.
|
|
110
|
+
*/
|
|
95
111
|
async function executeShutdownPhase(ctx, phase) {
|
|
96
112
|
const plan = buildExecutionPlan(ctx.registry.getAll(), phase);
|
|
97
113
|
const context = createLifecycleContext(phase, ctx.startTime, ctx.controller.signal);
|
|
98
|
-
const
|
|
114
|
+
const isStop = phase === LifecyclePhase.STOP;
|
|
115
|
+
const prerequisite = isStop
|
|
116
|
+
? LifecyclePhase.START
|
|
117
|
+
: LifecyclePhase.INITIALIZE;
|
|
118
|
+
const successState = isStop
|
|
99
119
|
? LifecycleState.STOPPED
|
|
100
120
|
: LifecycleState.DISPOSED;
|
|
101
121
|
for (const stage of plan.stages) {
|
|
@@ -104,13 +124,30 @@ async function executeShutdownPhase(ctx, phase) {
|
|
|
104
124
|
.filter((r) => r !== undefined);
|
|
105
125
|
if (stageRegs.length === 0)
|
|
106
126
|
continue;
|
|
127
|
+
const runnable = [];
|
|
107
128
|
for (const reg of stageRegs) {
|
|
108
|
-
|
|
129
|
+
if (!wasAttempted(ctx, reg.id, prerequisite)) {
|
|
130
|
+
// Never reached the phase this hook undoes. It still ends up
|
|
131
|
+
// DISPOSED so status reflects the teardown.
|
|
132
|
+
if (!isStop) {
|
|
133
|
+
transitionComponent(ctx, reg.id, LifecycleState.DISPOSED);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
runnable.push(reg);
|
|
138
|
+
// Only the stop phase moves a component into STOPPING; dispose
|
|
139
|
+
// runs from STOPPED (or FAILED) and transitions straight to
|
|
140
|
+
// DISPOSED.
|
|
141
|
+
if (isStop) {
|
|
142
|
+
transitionComponent(ctx, reg.id, LifecycleState.STOPPING);
|
|
143
|
+
}
|
|
109
144
|
ctx.events.emit("component:stopping", {
|
|
110
145
|
component: { componentId: reg.id },
|
|
111
146
|
});
|
|
112
147
|
}
|
|
113
|
-
|
|
148
|
+
if (runnable.length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
const results = await ctx.executor.executeStage(runnable, phase, context, ctx.concurrency);
|
|
114
151
|
// Shutdown results used to be discarded entirely: a component whose
|
|
115
152
|
// stop() or dispose() threw was still reported as cleanly STOPPED,
|
|
116
153
|
// its failure never reached getStatus() or the event stream, and
|
|
@@ -118,7 +155,7 @@ async function executeShutdownPhase(ctx, phase) {
|
|
|
118
155
|
for (const result of results) {
|
|
119
156
|
recordResult(ctx, result);
|
|
120
157
|
if (result.success) {
|
|
121
|
-
transitionComponent(ctx, result.id,
|
|
158
|
+
transitionComponent(ctx, result.id, successState);
|
|
122
159
|
ctx.events.emit("component:stopped", {
|
|
123
160
|
component: { componentId: result.id, duration: result.duration },
|
|
124
161
|
});
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Startup orchestration — initializes, starts, and readies components.
|
|
5
5
|
*/
|
|
6
6
|
import { LifecyclePhase, LifecycleState } from "@zudojs/constants";
|
|
7
|
-
import { LifecycleStartError } from "@zudojs/errors";
|
|
7
|
+
import { LifecycleComponentError, LifecycleStartError } from "@zudojs/errors";
|
|
8
8
|
import { buildExecutionPlan } from "../lifecyclePlan/lifecyclePlan.core.js";
|
|
9
9
|
import { createLifecycleContext } from "../lifecycleContext/lifecycleContext.type.js";
|
|
10
|
-
import { transitionComponent, transitionComponentBatch, emitComponentFailed, recordResult, } from "./lifecycleManager.context.js";
|
|
10
|
+
import { transitionComponent, transitionComponentBatch, emitComponentFailed, failComponent, markAttempted, recordResult, } from "./lifecycleManager.context.js";
|
|
11
11
|
import { performShutdown } from "./lifecycleManager.shutdown.js";
|
|
12
12
|
/** Maps startup phases to their target component state after success. */
|
|
13
13
|
const SUCCESS_STATE = {
|
|
@@ -68,6 +68,7 @@ export async function performStartup(ctx) {
|
|
|
68
68
|
if (failedInit) {
|
|
69
69
|
throw new LifecycleStartError(failedInit.id, failedInit.error);
|
|
70
70
|
}
|
|
71
|
+
assertNotShuttingDown(ctx, LifecyclePhase.INITIALIZE);
|
|
71
72
|
ctx.state.transition(LifecycleState.INITIALIZED);
|
|
72
73
|
ctx.events.emit("application:initialized", {
|
|
73
74
|
duration: Date.now() - ctx.startTime,
|
|
@@ -77,6 +78,7 @@ export async function performStartup(ctx) {
|
|
|
77
78
|
if (failedStart) {
|
|
78
79
|
throw new LifecycleStartError(failedStart.id, failedStart.error);
|
|
79
80
|
}
|
|
81
|
+
assertNotShuttingDown(ctx, LifecyclePhase.START);
|
|
80
82
|
ctx.state.transition(LifecycleState.STARTED);
|
|
81
83
|
// A failing `ready` hook on a critical component used to be
|
|
82
84
|
// ignored completely: no state change, no rollback, and start()
|
|
@@ -85,6 +87,7 @@ export async function performStartup(ctx) {
|
|
|
85
87
|
if (failedReady) {
|
|
86
88
|
throw new LifecycleStartError(failedReady.id, failedReady.error);
|
|
87
89
|
}
|
|
90
|
+
assertNotShuttingDown(ctx, LifecyclePhase.READY);
|
|
88
91
|
ctx.state.transition(LifecycleState.READY);
|
|
89
92
|
ctx.events.emit("application:ready", {
|
|
90
93
|
duration: Date.now() - ctx.startTime,
|
|
@@ -94,7 +97,10 @@ export async function performStartup(ctx) {
|
|
|
94
97
|
// Rollback happens on exactly one path, so a completed teardown is
|
|
95
98
|
// never re-entered and its DISPOSED state is never overwritten
|
|
96
99
|
// with FAILED.
|
|
97
|
-
|
|
100
|
+
// When the failure IS a requested shutdown, the teardown already
|
|
101
|
+
// owns the application state.
|
|
102
|
+
if (ctx.shutdownPromise === undefined &&
|
|
103
|
+
ctx.state.state !== LifecycleState.FAILED &&
|
|
98
104
|
ctx.state.state !== LifecycleState.DISPOSED) {
|
|
99
105
|
ctx.state.forceState(LifecycleState.FAILED);
|
|
100
106
|
}
|
|
@@ -102,9 +108,40 @@ export async function performStartup(ctx) {
|
|
|
102
108
|
throw error;
|
|
103
109
|
}
|
|
104
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Throws when a shutdown has been requested while startup is running.
|
|
113
|
+
*
|
|
114
|
+
* Startup used to keep launching later stages after `shutdown()` had
|
|
115
|
+
* already torn everything down: a component started that way was
|
|
116
|
+
* never stopped, and the eventual failure was an opaque
|
|
117
|
+
* LifecycleStateError from the DISPOSED → INITIALIZED transition.
|
|
118
|
+
*/
|
|
119
|
+
function assertNotShuttingDown(ctx, phase) {
|
|
120
|
+
if (ctx.shutdownPromise !== undefined) {
|
|
121
|
+
throw new LifecycleStartError("application", new LifecycleComponentError("application", phase, new Error("Startup was cancelled because shutdown was requested while the application was starting.")));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
105
124
|
/**
|
|
106
125
|
* Executes a single startup phase across all registered components.
|
|
107
126
|
* Returns the failure of the first critical component, or undefined.
|
|
127
|
+
*
|
|
128
|
+
* Two bookkeeping rules apply to every stage:
|
|
129
|
+
*
|
|
130
|
+
* - A component that FAILED an earlier phase, or whose dependency has
|
|
131
|
+
* failed, does not enter this phase. It used to have `start()` and
|
|
132
|
+
* `ready()` invoked after its own `initialize()` had thrown, and its
|
|
133
|
+
* dependents were started as if the dependency were healthy —
|
|
134
|
+
* silently voiding the `dependsOn` contract. A skipped dependent is
|
|
135
|
+
* recorded as FAILED with a LifecycleComponentError naming the
|
|
136
|
+
* failed dependency, and its own `critical` flag decides whether
|
|
137
|
+
* startup aborts.
|
|
138
|
+
*
|
|
139
|
+
* - Every result of a stage is recorded, transitioned and announced
|
|
140
|
+
* before a critical failure aborts the phase. Returning on the first
|
|
141
|
+
* failed result dropped the results of siblings in the same stage,
|
|
142
|
+
* which were then left in INITIALIZING / STARTING forever (no
|
|
143
|
+
* transition leads out of those states except to their success or
|
|
144
|
+
* FAILED) even after rollback had disposed them.
|
|
108
145
|
*/
|
|
109
146
|
async function executePhase(ctx, phase) {
|
|
110
147
|
const plan = buildExecutionPlan(ctx.registry.getAll(), phase);
|
|
@@ -117,21 +154,62 @@ async function executePhase(ctx, phase) {
|
|
|
117
154
|
.filter((r) => r !== undefined);
|
|
118
155
|
if (stageRegs.length === 0)
|
|
119
156
|
continue;
|
|
120
|
-
|
|
157
|
+
assertNotShuttingDown(ctx, phase);
|
|
158
|
+
const runnable = [];
|
|
159
|
+
let criticalFailure;
|
|
121
160
|
for (const reg of stageRegs) {
|
|
161
|
+
if (ctx.componentStates.get(reg.id)?.state === LifecycleState.FAILED) {
|
|
162
|
+
// Already failed in an earlier phase; nothing more to run.
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const failedDependency = reg.dependsOn.find((dep) => ctx.componentStates.get(dep)?.state === LifecycleState.FAILED);
|
|
166
|
+
if (failedDependency === undefined) {
|
|
167
|
+
runnable.push(reg);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const skipped = {
|
|
171
|
+
id: reg.id,
|
|
172
|
+
phase,
|
|
173
|
+
duration: 0,
|
|
174
|
+
success: false,
|
|
175
|
+
error: new LifecycleComponentError(reg.id, phase, new Error(`Component "${reg.id}" was not started because its dependency "${failedDependency}" failed.`)),
|
|
176
|
+
};
|
|
177
|
+
recordResult(ctx, skipped);
|
|
178
|
+
failComponent(ctx, reg.id);
|
|
179
|
+
emitComponentFailed(ctx, skipped);
|
|
180
|
+
if (reg.critical) {
|
|
181
|
+
criticalFailure ??= { id: reg.id, error: skipped.error };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (criticalFailure) {
|
|
185
|
+
ctx.state.forceState(LifecycleState.FAILED);
|
|
186
|
+
return criticalFailure;
|
|
187
|
+
}
|
|
188
|
+
if (runnable.length === 0)
|
|
189
|
+
continue;
|
|
190
|
+
transitionComponentBatch(ctx, runnable.map((r) => r.id), EXECUTING_STATE[phase]);
|
|
191
|
+
for (const reg of runnable) {
|
|
192
|
+
markAttempted(ctx, reg.id, phase);
|
|
122
193
|
ctx.events.emit(events.begin, {
|
|
123
194
|
component: { componentId: reg.id },
|
|
124
195
|
});
|
|
125
196
|
}
|
|
126
|
-
const
|
|
197
|
+
const pending = ctx.executor.executeStage(runnable, phase, context, ctx.concurrency);
|
|
198
|
+
ctx.inFlight = pending;
|
|
199
|
+
let results;
|
|
200
|
+
try {
|
|
201
|
+
results = await pending;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
ctx.inFlight = undefined;
|
|
205
|
+
}
|
|
127
206
|
for (const result of results) {
|
|
128
207
|
recordResult(ctx, result);
|
|
129
208
|
if (!result.success) {
|
|
130
209
|
transitionComponent(ctx, result.id, LifecycleState.FAILED);
|
|
131
210
|
emitComponentFailed(ctx, result);
|
|
132
211
|
if (ctx.registry.get(result.id)?.critical) {
|
|
133
|
-
|
|
134
|
-
return { id: result.id, error: result.error };
|
|
212
|
+
criticalFailure ??= { id: result.id, error: result.error };
|
|
135
213
|
}
|
|
136
214
|
}
|
|
137
215
|
else {
|
|
@@ -141,6 +219,10 @@ async function executePhase(ctx, phase) {
|
|
|
141
219
|
});
|
|
142
220
|
}
|
|
143
221
|
}
|
|
222
|
+
if (criticalFailure) {
|
|
223
|
+
ctx.state.forceState(LifecycleState.FAILED);
|
|
224
|
+
return criticalFailure;
|
|
225
|
+
}
|
|
144
226
|
}
|
|
145
227
|
return undefined;
|
|
146
228
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/lifecycle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Application and component lifecycle orchestration with state machine, dependency ordering, graceful shutdown, rollback, and signals.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -20,8 +24,8 @@
|
|
|
20
24
|
"!dist/.tsbuildinfo"
|
|
21
25
|
],
|
|
22
26
|
"dependencies": {
|
|
23
|
-
"@zudojs/errors": "1.0.
|
|
24
|
-
"@zudojs/constants": "1.0.
|
|
27
|
+
"@zudojs/errors": "1.0.1",
|
|
28
|
+
"@zudojs/constants": "1.0.1"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"@types/node": "^26.4.1",
|