@zudojs/lifecycle 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 +100 -12
- package/dist/lifecycleExecutor/lifecycleExecutor.core.js +36 -5
- package/dist/lifecycleInternal/asyncUtils.core.js +58 -14
- package/dist/lifecycleManager/lifecycleManager.context.d.ts +53 -0
- package/dist/lifecycleManager/lifecycleManager.context.js +25 -0
- package/dist/lifecycleManager/lifecycleManager.core.d.ts +6 -2
- package/dist/lifecycleManager/lifecycleManager.core.js +12 -7
- package/dist/lifecycleManager/lifecycleManager.shutdown.d.ts +4 -1
- package/dist/lifecycleManager/lifecycleManager.shutdown.js +123 -14
- package/dist/lifecycleManager/lifecycleManager.startup.d.ts +6 -0
- package/dist/lifecycleManager/lifecycleManager.startup.js +165 -36
- package/dist/lifecyclePhase/lifecyclePhase.core.d.ts +7 -1
- package/dist/lifecyclePhase/lifecyclePhase.core.js +8 -13
- package/dist/lifecyclePlan/lifecyclePlan.core.js +7 -3
- package/dist/lifecycleSignal/lifecycleSignal.handler.d.ts +2 -2
- package/dist/lifecycleSignal/lifecycleSignal.handler.js +6 -3
- package/package.json +25 -14
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycleComponent/index.d.ts.map +0 -1
- package/dist/lifecycleComponent/index.js.map +0 -1
- package/dist/lifecycleComponent/lifecycleComponent.type.d.ts.map +0 -1
- package/dist/lifecycleComponent/lifecycleComponent.type.js.map +0 -1
- package/dist/lifecycleContext/index.d.ts.map +0 -1
- package/dist/lifecycleContext/index.js.map +0 -1
- package/dist/lifecycleContext/lifecycleContext.type.d.ts.map +0 -1
- package/dist/lifecycleContext/lifecycleContext.type.js.map +0 -1
- package/dist/lifecycleEvents/index.d.ts.map +0 -1
- package/dist/lifecycleEvents/index.js.map +0 -1
- package/dist/lifecycleEvents/lifecycleEvents.core.d.ts.map +0 -1
- package/dist/lifecycleEvents/lifecycleEvents.core.js.map +0 -1
- package/dist/lifecycleExecutor/index.d.ts.map +0 -1
- package/dist/lifecycleExecutor/index.js.map +0 -1
- package/dist/lifecycleExecutor/lifecycleExecutor.core.d.ts.map +0 -1
- package/dist/lifecycleExecutor/lifecycleExecutor.core.js.map +0 -1
- package/dist/lifecycleInternal/asyncUtils.core.d.ts.map +0 -1
- package/dist/lifecycleInternal/asyncUtils.core.js.map +0 -1
- package/dist/lifecycleInternal/dependencyGraph.core.d.ts.map +0 -1
- package/dist/lifecycleInternal/dependencyGraph.core.js.map +0 -1
- package/dist/lifecycleInternal/index.d.ts.map +0 -1
- package/dist/lifecycleInternal/index.js.map +0 -1
- package/dist/lifecycleInternal/topologicalSort.core.d.ts.map +0 -1
- package/dist/lifecycleInternal/topologicalSort.core.js.map +0 -1
- package/dist/lifecycleManager/index.d.ts.map +0 -1
- package/dist/lifecycleManager/index.js.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.context.d.ts.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.context.js.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.core.d.ts.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.core.js.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.shutdown.d.ts.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.shutdown.js.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.startup.d.ts.map +0 -1
- package/dist/lifecycleManager/lifecycleManager.startup.js.map +0 -1
- package/dist/lifecyclePhase/index.d.ts.map +0 -1
- package/dist/lifecyclePhase/index.js.map +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.core.d.ts.map +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.core.js.map +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.type.d.ts +0 -16
- package/dist/lifecyclePhase/lifecyclePhase.type.d.ts.map +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.type.js +0 -17
- package/dist/lifecyclePhase/lifecyclePhase.type.js.map +0 -1
- package/dist/lifecyclePlan/index.d.ts.map +0 -1
- package/dist/lifecyclePlan/index.js.map +0 -1
- package/dist/lifecyclePlan/lifecyclePlan.core.d.ts.map +0 -1
- package/dist/lifecyclePlan/lifecyclePlan.core.js.map +0 -1
- package/dist/lifecycleRegistry/index.d.ts.map +0 -1
- package/dist/lifecycleRegistry/index.js.map +0 -1
- package/dist/lifecycleRegistry/lifecycleRegistry.core.d.ts.map +0 -1
- package/dist/lifecycleRegistry/lifecycleRegistry.core.js.map +0 -1
- package/dist/lifecycleSignal/index.d.ts.map +0 -1
- package/dist/lifecycleSignal/index.js.map +0 -1
- package/dist/lifecycleSignal/lifecycleSignal.handler.d.ts.map +0 -1
- package/dist/lifecycleSignal/lifecycleSignal.handler.js.map +0 -1
- package/dist/lifecycleState/index.d.ts.map +0 -1
- package/dist/lifecycleState/index.js.map +0 -1
- package/dist/lifecycleState/lifecycleState.machine.d.ts.map +0 -1
- package/dist/lifecycleState/lifecycleState.machine.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zudojs Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @zudojs/lifecycle
|
|
2
2
|
|
|
3
|
-
Application and component lifecycle orchestration with state machine,
|
|
3
|
+
Application and component lifecycle orchestration with a state machine,
|
|
4
|
+
dependency ordering, graceful shutdown, rollback, and signal handling.
|
|
4
5
|
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
@@ -13,22 +14,109 @@ npm install @zudojs/lifecycle
|
|
|
13
14
|
```typescript
|
|
14
15
|
import { createLifecycleManager } from "@zudojs/lifecycle";
|
|
15
16
|
|
|
16
|
-
const manager = createLifecycleManager(
|
|
17
|
-
|
|
18
|
-
});
|
|
17
|
+
const manager = createLifecycleManager();
|
|
18
|
+
|
|
19
|
+
manager.register(database, { id: "db" });
|
|
20
|
+
manager.register(queue, { id: "queue", dependsOn: ["db"] });
|
|
21
|
+
manager.register(server, { id: "server", dependsOn: ["queue"] });
|
|
19
22
|
|
|
20
23
|
await manager.start();
|
|
21
|
-
|
|
24
|
+
// ... application running ...
|
|
25
|
+
await manager.shutdown();
|
|
26
|
+
manager.dispose();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Components are plain objects implementing any subset of the hooks:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const database = {
|
|
33
|
+
name: "database",
|
|
34
|
+
async initialize(context) {},
|
|
35
|
+
async start(context) {},
|
|
36
|
+
async ready(context) {},
|
|
37
|
+
async stop(context) {},
|
|
38
|
+
async dispose(context) {},
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Startup and rollback
|
|
43
|
+
|
|
44
|
+
Phases run in order: `initialize` → `start` → `ready`. Components with
|
|
45
|
+
no dependency relationship run in parallel (bounded by `concurrency`).
|
|
46
|
+
|
|
47
|
+
If a **critical** component (the default) fails any startup phase,
|
|
48
|
+
`start()` rolls the application back (`stop` → `dispose`) and then
|
|
49
|
+
**rejects** with a `LifecycleStartError`. Register a component with
|
|
50
|
+
`{ critical: false }` when its failure should not abort startup — the
|
|
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.
|
|
64
|
+
|
|
65
|
+
## Shutdown
|
|
66
|
+
|
|
67
|
+
`shutdown()` runs `stop` → `dispose` in reverse dependency order. It is
|
|
68
|
+
single-flight: concurrent callers, including the rollback triggered by a
|
|
69
|
+
failing startup and the process signal handler, all await the same run.
|
|
70
|
+
|
|
71
|
+
`shutdownTimeout` (default 30s) is a real wall-clock deadline for the
|
|
72
|
+
whole sequence. When it expires the lifecycle context's `AbortSignal` is
|
|
73
|
+
aborted so hooks that observe it can unwind, and shutdown completes
|
|
74
|
+
regardless. A component that ignores the signal is abandoned, not
|
|
75
|
+
awaited forever.
|
|
76
|
+
|
|
77
|
+
Failing `stop()`/`dispose()` hooks are recorded: the component is marked
|
|
78
|
+
`FAILED`, a `component:failed` event is emitted, and the result appears
|
|
79
|
+
in `getStatus()`.
|
|
80
|
+
|
|
81
|
+
## Cancellation
|
|
82
|
+
|
|
83
|
+
Every hook receives a `LifecycleContext` whose `signal` is shared by the
|
|
84
|
+
whole run and is aborted when the shutdown deadline expires. Long-running
|
|
85
|
+
hooks should honour it:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
async stop(context) {
|
|
89
|
+
await drain({ signal: context.signal });
|
|
90
|
+
}
|
|
22
91
|
```
|
|
23
92
|
|
|
24
|
-
##
|
|
93
|
+
## Events
|
|
94
|
+
|
|
95
|
+
`manager.events.on(type, listener)` subscribes to:
|
|
96
|
+
|
|
97
|
+
- `component:registered`, `component:initializing`, `component:initialized`,
|
|
98
|
+
`component:starting`, `component:started`, `component:ready`,
|
|
99
|
+
`component:stopping`, `component:stopped`, `component:failed`
|
|
100
|
+
- `application:initializing`, `application:initialized`,
|
|
101
|
+
`application:starting`, `application:ready`, `application:stopping`,
|
|
102
|
+
`application:stopped`, `application:disposed`
|
|
103
|
+
|
|
104
|
+
Listener exceptions are swallowed so observability never breaks the
|
|
105
|
+
lifecycle.
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
createLifecycleManager({
|
|
111
|
+
concurrency: 10, // parallel component operations per stage
|
|
112
|
+
shutdownTimeout: 30_000, // global shutdown deadline (ms)
|
|
113
|
+
handleSignals: true, // install process signal handlers
|
|
114
|
+
signals: ["SIGINT", "SIGTERM"], // defaults to DEFAULT_SHUTDOWN_SIGNALS
|
|
115
|
+
});
|
|
116
|
+
```
|
|
25
117
|
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
- Graceful shutdown with timeouts
|
|
29
|
-
- Rollback on startup failure
|
|
30
|
-
- Signal handling
|
|
31
|
-
- Lifecycle hooks and events
|
|
118
|
+
Per-component: `id`, `dependsOn`, `priority`, `critical`, `timeout`,
|
|
119
|
+
`retry: { attempts, delay, maxDelay, backoff }`.
|
|
32
120
|
|
|
33
121
|
## Use Cases
|
|
34
122
|
|
|
@@ -29,6 +29,13 @@ export class LifecycleExecutor {
|
|
|
29
29
|
const retryConfig = registration.retry;
|
|
30
30
|
const maxAttempts = 1 + (retryConfig.attempts ?? 0);
|
|
31
31
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
32
|
+
// A hook must not be retried (or even started) once the run has
|
|
33
|
+
// been cancelled — the context signal is aborted by the shutdown
|
|
34
|
+
// deadline and by startup rollback.
|
|
35
|
+
if (context.signal.aborted) {
|
|
36
|
+
lastError ??= new LifecycleComponentError(registration.id, phase, context.signal.reason);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
32
39
|
try {
|
|
33
40
|
await withTimeout(async () => {
|
|
34
41
|
const result = hook.call(registration.component, context);
|
|
@@ -47,7 +54,7 @@ export class LifecycleExecutor {
|
|
|
47
54
|
lastError = error;
|
|
48
55
|
if (attempt < maxAttempts - 1) {
|
|
49
56
|
const delay = calculateDelay(retryConfig, attempt);
|
|
50
|
-
await sleep(delay);
|
|
57
|
+
await sleep(delay, context.signal);
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
}
|
|
@@ -55,7 +62,12 @@ export class LifecycleExecutor {
|
|
|
55
62
|
id: registration.id,
|
|
56
63
|
phase,
|
|
57
64
|
duration: Date.now() - startTime,
|
|
58
|
-
|
|
65
|
+
// LifecycleComponentError was imported but never constructed, so
|
|
66
|
+
// callers received a bare hook error with no indication of which
|
|
67
|
+
// component or phase produced it.
|
|
68
|
+
error: lastError instanceof LifecycleComponentError
|
|
69
|
+
? lastError
|
|
70
|
+
: new LifecycleComponentError(registration.id, phase, lastError),
|
|
59
71
|
success: false,
|
|
60
72
|
};
|
|
61
73
|
}
|
|
@@ -81,8 +93,27 @@ function calculateDelay(config, attempt) {
|
|
|
81
93
|
}
|
|
82
94
|
return base;
|
|
83
95
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Sleeps for the given duration, waking early when the run is aborted.
|
|
98
|
+
*
|
|
99
|
+
* An unconditional timer would keep the process alive for a full retry
|
|
100
|
+
* backoff after shutdown had already been requested.
|
|
101
|
+
*/
|
|
102
|
+
function sleep(ms, signal) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
if (signal.aborted) {
|
|
105
|
+
resolve();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
signal.removeEventListener("abort", onAbort);
|
|
110
|
+
resolve();
|
|
111
|
+
}, ms);
|
|
112
|
+
function onAbort() {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
116
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
117
|
+
});
|
|
87
118
|
}
|
|
88
119
|
//# sourceMappingURL=lifecycleExecutor.core.js.map
|
|
@@ -13,12 +13,23 @@ export async function withTimeout(fn, timeoutMs, componentId, phase) {
|
|
|
13
13
|
const timer = setTimeout(() => {
|
|
14
14
|
reject(new LifecycleTimeoutError(componentId, phase, timeoutMs));
|
|
15
15
|
}, timeoutMs);
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// The timer must be cleared on EVERY exit path. A synchronously
|
|
17
|
+
// throwing `fn` used to escape before `.catch` was attached,
|
|
18
|
+
// leaving an armed timer that kept the event loop alive for the
|
|
19
|
+
// whole timeout after the operation had already failed.
|
|
20
|
+
let operation;
|
|
21
|
+
try {
|
|
22
|
+
operation = fn();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
reject(error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
operation.then((result) => {
|
|
18
30
|
clearTimeout(timer);
|
|
19
31
|
resolve(result);
|
|
20
|
-
})
|
|
21
|
-
.catch((error) => {
|
|
32
|
+
}, (error) => {
|
|
22
33
|
clearTimeout(timer);
|
|
23
34
|
reject(error);
|
|
24
35
|
});
|
|
@@ -36,7 +47,19 @@ export async function withAbort(fn, signal) {
|
|
|
36
47
|
reject(new Error("Operation aborted"));
|
|
37
48
|
};
|
|
38
49
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
-
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
|
|
40
63
|
.then((result) => {
|
|
41
64
|
signal.removeEventListener("abort", onAbort);
|
|
42
65
|
resolve(result);
|
|
@@ -51,19 +74,40 @@ export async function withAbort(fn, signal) {
|
|
|
51
74
|
* Executes async operations with a concurrency limit.
|
|
52
75
|
*/
|
|
53
76
|
export async function withConcurrency(items, concurrency, fn) {
|
|
77
|
+
const limit = Number.isFinite(concurrency) && concurrency > 0
|
|
78
|
+
? Math.floor(concurrency)
|
|
79
|
+
: 1;
|
|
54
80
|
const executing = new Set();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
// Every scheduled task is settled before this function returns, and
|
|
82
|
+
// the FIRST failure is rethrown afterwards. The previous version
|
|
83
|
+
// removed a task from `executing` only on success, so one rejection
|
|
84
|
+
// left a permanently-rejected promise in the race set, abandoned the
|
|
85
|
+
// remaining items, and produced an unhandled rejection for every
|
|
86
|
+
// task still in flight.
|
|
87
|
+
const settled = [];
|
|
88
|
+
let firstError;
|
|
89
|
+
let failed = false;
|
|
90
|
+
// `entries()` yields the element typed as T, so no non-null
|
|
91
|
+
// assertion is needed to satisfy noUncheckedIndexedAccess.
|
|
92
|
+
for (const [index, item] of items.entries()) {
|
|
93
|
+
const task = fn(item, index).catch((error) => {
|
|
94
|
+
if (!failed) {
|
|
95
|
+
failed = true;
|
|
96
|
+
firstError = error;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const tracked = task.then(() => {
|
|
100
|
+
executing.delete(tracked);
|
|
60
101
|
});
|
|
61
|
-
executing.add(
|
|
62
|
-
|
|
63
|
-
if (executing.size >=
|
|
102
|
+
executing.add(tracked);
|
|
103
|
+
settled.push(tracked);
|
|
104
|
+
if (executing.size >= limit) {
|
|
64
105
|
await Promise.race(executing);
|
|
65
106
|
}
|
|
66
107
|
}
|
|
67
|
-
await Promise.all(
|
|
108
|
+
await Promise.all(settled);
|
|
109
|
+
if (failed) {
|
|
110
|
+
throw firstError;
|
|
111
|
+
}
|
|
68
112
|
}
|
|
69
113
|
//# sourceMappingURL=asyncUtils.core.js.map
|
|
@@ -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,13 +21,65 @@ 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;
|
|
35
|
+
/**
|
|
36
|
+
* Cancellation source for the current run.
|
|
37
|
+
*
|
|
38
|
+
* Its signal is the one handed to every component hook through
|
|
39
|
+
* LifecycleContext. It is aborted when the global shutdown deadline
|
|
40
|
+
* expires, so a hook that honours the signal can bail out instead of
|
|
41
|
+
* hanging the process.
|
|
42
|
+
*/
|
|
43
|
+
readonly controller: AbortController;
|
|
44
|
+
/**
|
|
45
|
+
* The in-flight shutdown, shared by every caller.
|
|
46
|
+
*
|
|
47
|
+
* Startup rollback and LifecycleManager.shutdown() both go through
|
|
48
|
+
* this field, so a signal arriving during a failing startup joins the
|
|
49
|
+
* rollback already running instead of starting a second, overlapping
|
|
50
|
+
* teardown.
|
|
51
|
+
*/
|
|
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>;
|
|
24
63
|
}
|
|
64
|
+
/** Records an execution result against its component. */
|
|
65
|
+
export declare function recordResult(ctx: LifecycleManagerContext, result: ExecutionResult): void;
|
|
25
66
|
/**
|
|
26
67
|
* Safely transitions a component state machine to the target state.
|
|
27
68
|
* No-op if the transition is not valid from the current state.
|
|
28
69
|
*/
|
|
29
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;
|
|
30
83
|
/**
|
|
31
84
|
* Transitions multiple component state machines to the same target state.
|
|
32
85
|
*/
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* plus helper functions for component state transitions and event emission.
|
|
6
6
|
*/
|
|
7
7
|
import { LifecycleState } from "@zudojs/constants";
|
|
8
|
+
/** Records an execution result against its component. */
|
|
9
|
+
export function recordResult(ctx, result) {
|
|
10
|
+
const existing = ctx.results.get(result.id) ?? [];
|
|
11
|
+
ctx.results.set(result.id, [...existing, result]);
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Safely transitions a component state machine to the target state.
|
|
10
15
|
* No-op if the transition is not valid from the current state.
|
|
@@ -15,6 +20,26 @@ export function transitionComponent(ctx, id, targetState) {
|
|
|
15
20
|
sm.transition(targetState);
|
|
16
21
|
}
|
|
17
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
|
+
}
|
|
18
43
|
/**
|
|
19
44
|
* Transitions multiple component state machines to the same target state.
|
|
20
45
|
*/
|
|
@@ -36,7 +36,6 @@ export interface LifecycleManagerOptions {
|
|
|
36
36
|
export declare class LifecycleManager {
|
|
37
37
|
private readonly _ctx;
|
|
38
38
|
private _startPromise?;
|
|
39
|
-
private _shutdownPromise?;
|
|
40
39
|
private _removeSignalHandlers?;
|
|
41
40
|
constructor(options?: LifecycleManagerOptions);
|
|
42
41
|
/** Registers a component with the lifecycle manager. */
|
|
@@ -62,7 +61,12 @@ export declare class LifecycleManager {
|
|
|
62
61
|
state: LifecycleState;
|
|
63
62
|
results: readonly ExecutionResult[];
|
|
64
63
|
}>;
|
|
65
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Disposes the lifecycle manager and cleans up resources.
|
|
66
|
+
*
|
|
67
|
+
* This removes signal handlers and listeners only; it does NOT run
|
|
68
|
+
* component teardown — call `shutdown()` first for that.
|
|
69
|
+
*/
|
|
66
70
|
dispose(): void;
|
|
67
71
|
}
|
|
68
72
|
/** Creates a new lifecycle manager. */
|
|
@@ -28,7 +28,6 @@ import { performShutdown } from "./lifecycleManager.shutdown.js";
|
|
|
28
28
|
export class LifecycleManager {
|
|
29
29
|
_ctx;
|
|
30
30
|
_startPromise;
|
|
31
|
-
_shutdownPromise;
|
|
32
31
|
_removeSignalHandlers;
|
|
33
32
|
constructor(options = {}) {
|
|
34
33
|
this._ctx = {
|
|
@@ -40,7 +39,9 @@ export class LifecycleManager {
|
|
|
40
39
|
shutdownTimeout: options.shutdownTimeout ?? LIFECYCLE_DEFAULT_SHUTDOWN_TIMEOUT,
|
|
41
40
|
componentStates: new Map(),
|
|
42
41
|
results: new Map(),
|
|
42
|
+
attempted: new Map(),
|
|
43
43
|
startTime: 0,
|
|
44
|
+
controller: new AbortController(),
|
|
44
45
|
};
|
|
45
46
|
if (options.handleSignals !== false) {
|
|
46
47
|
this._removeSignalHandlers = installSignalHandlers({
|
|
@@ -77,11 +78,9 @@ export class LifecycleManager {
|
|
|
77
78
|
* Idempotent — returns the same promise if called multiple times.
|
|
78
79
|
*/
|
|
79
80
|
async shutdown() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this._shutdownPromise = performShutdown(this._ctx);
|
|
84
|
-
return this._shutdownPromise;
|
|
81
|
+
// performShutdown is itself single-flight, so a shutdown started by
|
|
82
|
+
// startup rollback and one started here are the SAME run.
|
|
83
|
+
return performShutdown(this._ctx);
|
|
85
84
|
}
|
|
86
85
|
/** Returns the current application state. */
|
|
87
86
|
get state() {
|
|
@@ -106,9 +105,15 @@ export class LifecycleManager {
|
|
|
106
105
|
}
|
|
107
106
|
return status;
|
|
108
107
|
}
|
|
109
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Disposes the lifecycle manager and cleans up resources.
|
|
110
|
+
*
|
|
111
|
+
* This removes signal handlers and listeners only; it does NOT run
|
|
112
|
+
* component teardown — call `shutdown()` first for that.
|
|
113
|
+
*/
|
|
110
114
|
dispose() {
|
|
111
115
|
this._removeSignalHandlers?.();
|
|
116
|
+
this._removeSignalHandlers = undefined;
|
|
112
117
|
this._ctx.events.clear();
|
|
113
118
|
}
|
|
114
119
|
}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import type { LifecycleManagerContext } from "./lifecycleManager.context.js";
|
|
7
7
|
/**
|
|
8
8
|
* Performs the full shutdown sequence: stop → dispose.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* Idempotent and single-flight: concurrent callers (a signal handler
|
|
11
|
+
* and a failing startup, say) all await the SAME teardown rather than
|
|
12
|
+
* running overlapping ones.
|
|
10
13
|
*/
|
|
11
14
|
export declare function performShutdown(ctx: LifecycleManagerContext): Promise<void>;
|
|
12
15
|
//# sourceMappingURL=lifecycleManager.shutdown.d.ts.map
|