@zudojs/lifecycle 1.2.3 → 1.3.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 +44 -15
- package/dist/index.d.ts +2 -3
- package/dist/index.js +7 -1
- package/dist/lifecycleComponent/index.d.ts +0 -1
- package/dist/lifecycleComponent/index.js +0 -1
- package/dist/lifecycleComponent/lifecycleComponent.type.d.ts +27 -8
- package/dist/lifecycleComponent/lifecycleComponent.type.js +0 -1
- package/dist/lifecycleContext/index.d.ts +0 -1
- package/dist/lifecycleContext/index.js +0 -1
- package/dist/lifecycleContext/lifecycleContext.type.d.ts +0 -1
- package/dist/lifecycleContext/lifecycleContext.type.js +0 -1
- package/dist/lifecycleEvents/index.d.ts +0 -1
- package/dist/lifecycleEvents/index.js +0 -1
- package/dist/lifecycleEvents/lifecycleEvents.core.d.ts +20 -5
- package/dist/lifecycleEvents/lifecycleEvents.core.js +0 -1
- package/dist/lifecycleExecutor/index.d.ts +3 -3
- package/dist/lifecycleExecutor/index.js +2 -2
- package/dist/lifecycleExecutor/lifecycleExecutor.abandoned.d.ts +31 -0
- package/dist/lifecycleExecutor/lifecycleExecutor.abandoned.js +75 -0
- package/dist/lifecycleExecutor/lifecycleExecutor.core.d.ts +18 -24
- package/dist/lifecycleExecutor/lifecycleExecutor.core.js +64 -105
- package/dist/lifecycleExecutor/lifecycleExecutor.retry.d.ts +30 -0
- package/dist/lifecycleExecutor/lifecycleExecutor.retry.js +66 -0
- package/dist/lifecycleExecutor/lifecycleExecutor.type.d.ts +50 -0
- package/dist/lifecycleExecutor/lifecycleExecutor.type.js +6 -0
- package/dist/lifecycleInternal/asyncUtils.core.d.ts +0 -1
- package/dist/lifecycleInternal/asyncUtils.core.js +0 -1
- package/dist/lifecycleInternal/dependencyGraph.core.d.ts +34 -7
- package/dist/lifecycleInternal/dependencyGraph.core.js +55 -38
- package/dist/lifecycleInternal/dependencyGraph.cycle.d.ts +20 -0
- package/dist/lifecycleInternal/dependencyGraph.cycle.js +57 -0
- package/dist/lifecycleInternal/index.d.ts +1 -1
- package/dist/lifecycleInternal/index.js +0 -1
- package/dist/lifecycleInternal/timeoutBudget.core.d.ts +0 -1
- package/dist/lifecycleInternal/timeoutBudget.core.js +0 -1
- package/dist/lifecycleInternal/topologicalSort.core.d.ts +12 -2
- package/dist/lifecycleInternal/topologicalSort.core.js +41 -22
- package/dist/lifecycleManager/index.d.ts +0 -1
- package/dist/lifecycleManager/index.js +0 -1
- package/dist/lifecycleManager/lifecycleManager.context.d.ts +15 -7
- package/dist/lifecycleManager/lifecycleManager.context.js +0 -1
- package/dist/lifecycleManager/lifecycleManager.core.d.ts +12 -2
- package/dist/lifecycleManager/lifecycleManager.core.js +30 -3
- package/dist/lifecycleManager/lifecycleManager.deadline.d.ts +29 -0
- package/dist/lifecycleManager/lifecycleManager.deadline.js +79 -0
- package/dist/lifecycleManager/lifecycleManager.shutdown.d.ts +0 -1
- package/dist/lifecycleManager/lifecycleManager.shutdown.js +60 -69
- package/dist/lifecycleManager/lifecycleManager.startup.d.ts +0 -1
- package/dist/lifecycleManager/lifecycleManager.startup.js +12 -4
- package/dist/lifecyclePhase/index.d.ts +0 -1
- package/dist/lifecyclePhase/index.js +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.core.d.ts +0 -1
- package/dist/lifecyclePhase/lifecyclePhase.core.js +0 -1
- package/dist/lifecyclePlan/index.d.ts +0 -1
- package/dist/lifecyclePlan/index.js +0 -1
- package/dist/lifecyclePlan/lifecyclePlan.core.d.ts +0 -1
- package/dist/lifecyclePlan/lifecyclePlan.core.js +0 -2
- package/dist/lifecycleRegistry/index.d.ts +0 -1
- package/dist/lifecycleRegistry/index.js +0 -1
- package/dist/lifecycleRegistry/lifecycleRegistry.core.d.ts +0 -1
- package/dist/lifecycleRegistry/lifecycleRegistry.core.js +3 -2
- package/dist/lifecycleSignal/index.d.ts +0 -1
- package/dist/lifecycleSignal/index.js +0 -1
- package/dist/lifecycleSignal/lifecycleSignal.handler.d.ts +0 -1
- package/dist/lifecycleSignal/lifecycleSignal.handler.js +0 -1
- package/dist/lifecycleState/index.d.ts +0 -1
- package/dist/lifecycleState/index.js +0 -1
- package/dist/lifecycleState/lifecycleState.machine.d.ts +0 -1
- package/dist/lifecycleState/lifecycleState.machine.js +0 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -60,9 +60,11 @@ components that `dependsOn` it are not started either: they are marked
|
|
|
60
60
|
`FAILED` with a `LifecycleComponentError` naming the failed dependency,
|
|
61
61
|
and their own `critical` flag decides whether startup aborts.
|
|
62
62
|
|
|
63
|
-
Rollback only undoes phases that
|
|
64
|
-
whose `start`
|
|
65
|
-
`
|
|
63
|
+
Rollback only undoes phases that were reached: `stop()` is called on
|
|
64
|
+
components whose `start` completed (or timed out, so its outcome is
|
|
65
|
+
unknown) — never on one whose `start()` threw — and `dispose()` on
|
|
66
|
+
components whose `initialize` was invoked, even if it failed, because a
|
|
67
|
+
half-initialised component may still hold resources.
|
|
66
68
|
|
|
67
69
|
Calling `shutdown()` while `start()` is in flight waits for the
|
|
68
70
|
executing stage to settle, tears down, and makes `start()` reject with a
|
|
@@ -78,7 +80,11 @@ failing startup and the process signal handler, all await the same run.
|
|
|
78
80
|
whole sequence. When it expires the lifecycle context's `AbortSignal` is
|
|
79
81
|
aborted so hooks that observe it can unwind, and shutdown completes
|
|
80
82
|
regardless. A component that ignores the signal is abandoned, not
|
|
81
|
-
awaited forever.
|
|
83
|
+
awaited forever. Expiry is never silent: every component whose hook was
|
|
84
|
+
still running is marked `FAILED` with a `LifecycleTimeoutError` result,
|
|
85
|
+
one `application:shutdown-timeout` event is emitted (its `error` is the
|
|
86
|
+
timeout), `manager.shutdownTimedOut` reads `true`, and nothing is
|
|
87
|
+
emitted after `shutdown()` has resolved.
|
|
82
88
|
|
|
83
89
|
Failing `stop()`/`dispose()` hooks are recorded: the component is marked
|
|
84
90
|
`FAILED`, a `component:failed` event is emitted, and the result appears
|
|
@@ -86,9 +92,11 @@ in `getStatus()`.
|
|
|
86
92
|
|
|
87
93
|
## Cancellation
|
|
88
94
|
|
|
89
|
-
Every hook receives a `LifecycleContext` whose `signal` is
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
Every hook invocation receives a `LifecycleContext` whose `signal` is
|
|
96
|
+
derived from the run: it is aborted when that component's `timeout`
|
|
97
|
+
elapses and when the shutdown deadline expires. Long-running hooks
|
|
98
|
+
should honour it — a `start()` that does is what lets `start()` reject
|
|
99
|
+
at the component timeout instead of at the hook's own pace:
|
|
92
100
|
|
|
93
101
|
```typescript
|
|
94
102
|
async stop(context) {
|
|
@@ -101,11 +109,19 @@ async stop(context) {
|
|
|
101
109
|
`manager.events.on(type, listener)` subscribes to:
|
|
102
110
|
|
|
103
111
|
- `component:registered`, `component:initializing`, `component:initialized`,
|
|
104
|
-
`component:starting`, `component:started`, `component:
|
|
105
|
-
`component:
|
|
112
|
+
`component:starting`, `component:started`, `component:readying`,
|
|
113
|
+
`component:ready`, `component:retrying`, `component:stopping`,
|
|
114
|
+
`component:stopped`, `component:disposing`, `component:disposed`,
|
|
115
|
+
`component:failed`
|
|
106
116
|
- `application:initializing`, `application:initialized`,
|
|
107
|
-
`application:starting`, `application:
|
|
108
|
-
`application:
|
|
117
|
+
`application:starting`, `application:readying`, `application:ready`,
|
|
118
|
+
`application:stopping`, `application:shutdown-timeout`,
|
|
119
|
+
`application:stopped`, `application:disposing`, `application:disposed`
|
|
120
|
+
|
|
121
|
+
Every phase has its own begin/end pair (the ready and dispose phases
|
|
122
|
+
used to reuse `starting` and `stopping`/`stopped`). `component:retrying`
|
|
123
|
+
carries `component.attempt` (1-based), `component.delay` (ms) and the
|
|
124
|
+
`component.error` being retried.
|
|
109
125
|
|
|
110
126
|
Listener exceptions are swallowed so observability never breaks the
|
|
111
127
|
lifecycle.
|
|
@@ -134,10 +150,23 @@ stops first and the highest stops last.
|
|
|
134
150
|
`timeout` and `shutdownTimeout` accept `Infinity` for "no bound"; NaN and
|
|
135
151
|
negative values throw a `RangeError` when registered or constructed, and
|
|
136
152
|
finite values above 2^31-1 ms are clamped to the largest timer delay.
|
|
137
|
-
`retry`
|
|
138
|
-
|
|
139
|
-
`
|
|
140
|
-
|
|
153
|
+
`retry.attempts` is the number of retries after the first call, not the
|
|
154
|
+
total (`attempts: 3` allows four invocations); `delay` defaults to 500 ms,
|
|
155
|
+
`maxDelay` to 10 000 ms and `backoff` to `"exponential"` (doubling per
|
|
156
|
+
retry, capped at `maxDelay`). `retry` covers hooks that throw; a hook that
|
|
157
|
+
times out is not retried, because it is still running and a second call
|
|
158
|
+
would overlap it. Its `context.signal` is aborted and it is tracked per
|
|
159
|
+
component: before that component's `stop()`/`dispose()` the executor
|
|
160
|
+
waits for it — a `stop()` that overran its timeout is a drain and is
|
|
161
|
+
waited for until the shutdown deadline, a startup hook that ignored its
|
|
162
|
+
timeout only for one more `timeout` — so hooks of one component never
|
|
163
|
+
overlap and one hung `start()` no longer delays every other component's
|
|
164
|
+
teardown.
|
|
165
|
+
|
|
166
|
+
`DependencyGraph.addEdge` creates its endpoints, so a typo in a
|
|
167
|
+
dependency id sorts as a leaf. `getUndeclaredNodes()` lists such nodes
|
|
168
|
+
and `validate({ requireDeclared: true })` rejects them; the manager's
|
|
169
|
+
registry already rejects an unknown `dependsOn` at `start()`.
|
|
141
170
|
|
|
142
171
|
With `handleSignals`, SIGINT/SIGTERM listeners are installed by `start()`,
|
|
143
172
|
not by the constructor, and removed once shutdown finishes. A second
|
package/dist/index.d.ts
CHANGED
|
@@ -28,15 +28,14 @@ export type { LifecycleContext } from "./lifecycleContext/index.js";
|
|
|
28
28
|
export { createLifecycleContext } from "./lifecycleContext/index.js";
|
|
29
29
|
export { LifecycleRegistry } from "./lifecycleRegistry/index.js";
|
|
30
30
|
export { DependencyGraph, topologicalSort, reverseTopologicalSort, withTimeout, withAbort, withConcurrency, } from "./lifecycleInternal/index.js";
|
|
31
|
-
export type { TopologicalStage } from "./lifecycleInternal/index.js";
|
|
31
|
+
export type { DependencyGraphValidationOptions, TopologicalStage, } from "./lifecycleInternal/index.js";
|
|
32
32
|
export { buildExecutionPlan } from "./lifecyclePlan/index.js";
|
|
33
33
|
export type { ExecutionStage, ExecutionPlan } from "./lifecyclePlan/index.js";
|
|
34
34
|
export { LifecycleExecutor } from "./lifecycleExecutor/index.js";
|
|
35
|
-
export type { ExecutionResult } from "./lifecycleExecutor/index.js";
|
|
35
|
+
export type { ExecutionResult, LifecycleExecutorOptions, LifecycleRetryNotice, } from "./lifecycleExecutor/index.js";
|
|
36
36
|
export { LifecycleManager, createLifecycleManager, } from "./lifecycleManager/index.js";
|
|
37
37
|
export type { LifecycleManagerOptions } from "./lifecycleManager/index.js";
|
|
38
38
|
export { LifecycleEventEmitter } from "./lifecycleEvents/index.js";
|
|
39
39
|
export type { LifecycleEventType, LifecycleComponentEvent, LifecycleApplicationEvent, LifecycleEvent, LifecycleEventListener, } from "./lifecycleEvents/index.js";
|
|
40
40
|
export { installSignalHandlers, DEFAULT_SHUTDOWN_SIGNALS, } from "./lifecycleSignal/index.js";
|
|
41
41
|
export type { SignalHandlerOptions } from "./lifecycleSignal/index.js";
|
|
42
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -25,10 +25,16 @@ export { LifecycleStateMachine } from "./lifecycleState/index.js";
|
|
|
25
25
|
export { STARTUP_PHASES, SHUTDOWN_PHASES, getPhaseHookName, getComponentMethod, } from "./lifecyclePhase/index.js";
|
|
26
26
|
export { createLifecycleContext } from "./lifecycleContext/index.js";
|
|
27
27
|
export { LifecycleRegistry } from "./lifecycleRegistry/index.js";
|
|
28
|
+
/*
|
|
29
|
+
* Graph and async utilities. These live in the `lifecycleInternal`
|
|
30
|
+
* folder because the manager is their first consumer, but they are a
|
|
31
|
+
* supported part of the public API: `DependencyGraph` and the sorts
|
|
32
|
+
* are what `buildExecutionPlan` is built on, and `withTimeout` /
|
|
33
|
+
* `withAbort` / `withConcurrency` are reusable outside the manager.
|
|
34
|
+
*/
|
|
28
35
|
export { DependencyGraph, topologicalSort, reverseTopologicalSort, withTimeout, withAbort, withConcurrency, } from "./lifecycleInternal/index.js";
|
|
29
36
|
export { buildExecutionPlan } from "./lifecyclePlan/index.js";
|
|
30
37
|
export { LifecycleExecutor } from "./lifecycleExecutor/index.js";
|
|
31
38
|
export { LifecycleManager, createLifecycleManager, } from "./lifecycleManager/index.js";
|
|
32
39
|
export { LifecycleEventEmitter } from "./lifecycleEvents/index.js";
|
|
33
40
|
export { installSignalHandlers, DEFAULT_SHUTDOWN_SIGNALS, } from "./lifecycleSignal/index.js";
|
|
34
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -40,22 +40,42 @@ export interface LifecycleRegistrationOptions {
|
|
|
40
40
|
/** If true, application startup fails when this component fails. Defaults to true. */
|
|
41
41
|
readonly critical?: boolean;
|
|
42
42
|
/**
|
|
43
|
-
* Timeout in ms for
|
|
44
|
-
*
|
|
43
|
+
* Timeout in ms for each hook invocation. Defaults to
|
|
44
|
+
* `LIFECYCLE_DEFAULT_TIMEOUT` (30 s). `Infinity` means no bound; NaN
|
|
45
|
+
* and negative values are rejected at registration.
|
|
46
|
+
*
|
|
47
|
+
* When it elapses the hook's `context.signal` is aborted, the hook is
|
|
48
|
+
* recorded as failed with `timedOut: true` and is never retried, and
|
|
49
|
+
* startup rolls back. The same budget bounds the wait, before this
|
|
50
|
+
* component's `stop()`/`dispose()`, for an earlier hook of this
|
|
51
|
+
* component that is still running after its own timeout.
|
|
45
52
|
*/
|
|
46
53
|
readonly timeout?: number;
|
|
47
54
|
/** Retry configuration for failed operations. */
|
|
48
55
|
readonly retry?: LifecycleRetryOptions;
|
|
49
56
|
}
|
|
50
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Retry options for component operations.
|
|
59
|
+
*
|
|
60
|
+
* Retries apply to every phase hook. A hook that threw (or rejected)
|
|
61
|
+
* is retried after a delay; a hook that exceeded `timeout` is never
|
|
62
|
+
* retried, because it is still running and cannot be cancelled. Each
|
|
63
|
+
* retry is announced through the `component:retrying` event.
|
|
64
|
+
*/
|
|
51
65
|
export interface LifecycleRetryOptions {
|
|
52
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Number of retries after the first call, not the total number of
|
|
68
|
+
* calls: `attempts: 3` allows up to four invocations. Defaults to 0.
|
|
69
|
+
*/
|
|
53
70
|
readonly attempts?: number;
|
|
54
|
-
/** Delay
|
|
71
|
+
/** Delay before the first retry in ms. Defaults to 500. */
|
|
55
72
|
readonly delay?: number;
|
|
56
|
-
/**
|
|
73
|
+
/** Cap on the delay between retries in ms. Defaults to 10 000. */
|
|
57
74
|
readonly maxDelay?: number;
|
|
58
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Backoff strategy. `"exponential"` (the default) doubles `delay` on
|
|
77
|
+
* every retry up to `maxDelay`; `"fixed"` waits `delay` each time.
|
|
78
|
+
*/
|
|
59
79
|
readonly backoff?: "fixed" | "exponential";
|
|
60
80
|
}
|
|
61
81
|
/** A component with its registration metadata. */
|
|
@@ -75,4 +95,3 @@ export interface LifecycleRegistration {
|
|
|
75
95
|
/** Retry configuration. */
|
|
76
96
|
readonly retry: LifecycleRetryOptions;
|
|
77
97
|
}
|
|
78
|
-
//# sourceMappingURL=lifecycleComponent.type.d.ts.map
|
|
@@ -20,4 +20,3 @@ export interface LifecycleContext {
|
|
|
20
20
|
}
|
|
21
21
|
/** Creates a lifecycle context. */
|
|
22
22
|
export declare function createLifecycleContext(phase: LifecyclePhase, startedAt: number, signal?: AbortSignal, metadata?: Record<string, unknown>): LifecycleContext;
|
|
23
|
-
//# sourceMappingURL=lifecycleContext.type.d.ts.map
|
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lifecycle event emitter — emits typed events for observability integration.
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Lifecycle event types.
|
|
8
|
+
*
|
|
9
|
+
* Every phase has its own begin/end pair: the ready phase emits
|
|
10
|
+
* `component:readying` / `component:ready` and the dispose phase
|
|
11
|
+
* `component:disposing` / `component:disposed`. They used to reuse
|
|
12
|
+
* `component:starting` and `component:stopping`/`component:stopped`,
|
|
13
|
+
* so a listener could not tell the phases apart.
|
|
14
|
+
*/
|
|
15
|
+
export type LifecycleEventType = "component:registered" | "component:initializing" | "component:initialized" | "component:starting" | "component:started" | "component:readying" | "component:ready" | "component:retrying" | "component:stopping" | "component:stopped" | "component:disposing" | "component:disposed" | "component:failed" | "application:initializing" | "application:initialized" | "application:starting" | "application:readying" | "application:ready" | "application:stopping" | "application:shutdown-timeout" | "application:stopped" | "application:disposing" | "application:disposed";
|
|
8
16
|
/** Event payload for component events. */
|
|
9
17
|
export interface LifecycleComponentEvent {
|
|
10
18
|
/** Component ID. */
|
|
11
19
|
readonly componentId: string;
|
|
12
20
|
/** Duration in ms (for completion events). */
|
|
13
21
|
readonly duration?: number;
|
|
14
|
-
/** Error if the event represents a failure. */
|
|
22
|
+
/** Error if the event represents a failure (or the error being retried). */
|
|
15
23
|
readonly error?: unknown;
|
|
24
|
+
/** 1-based retry number (`component:retrying` only). */
|
|
25
|
+
readonly attempt?: number;
|
|
26
|
+
/** Milliseconds until that retry runs (`component:retrying` only). */
|
|
27
|
+
readonly delay?: number;
|
|
16
28
|
}
|
|
17
29
|
/** Event payload for application events. */
|
|
18
30
|
export interface LifecycleApplicationEvent {
|
|
@@ -29,7 +41,11 @@ export interface LifecycleEvent {
|
|
|
29
41
|
readonly component?: LifecycleComponentEvent;
|
|
30
42
|
/** Duration in ms (for completion events). */
|
|
31
43
|
readonly duration?: number;
|
|
32
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Error if the event represents a failure. For
|
|
46
|
+
* `application:shutdown-timeout` this is the LifecycleTimeoutError
|
|
47
|
+
* describing the expired deadline.
|
|
48
|
+
*/
|
|
33
49
|
readonly error?: unknown;
|
|
34
50
|
/** Timestamp. */
|
|
35
51
|
readonly timestamp: number;
|
|
@@ -49,4 +65,3 @@ export declare class LifecycleEventEmitter {
|
|
|
49
65
|
/** Removes all listeners. */
|
|
50
66
|
clear(): void;
|
|
51
67
|
}
|
|
52
|
-
//# sourceMappingURL=lifecycleEvents.core.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/lifecycle/executor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Runs component hooks with timeout, retry, per-invocation
|
|
5
|
+
* cancellation, and priority-aware concurrency.
|
|
5
6
|
*/
|
|
6
7
|
export { LifecycleExecutor } from "./lifecycleExecutor.core.js";
|
|
7
|
-
export type { ExecutionResult } from "./lifecycleExecutor.
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
8
|
+
export type { ExecutionResult, LifecycleExecutorOptions, LifecycleRetryNotice, } from "./lifecycleExecutor.type.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/lifecycle/executor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Runs component hooks with timeout, retry, per-invocation
|
|
5
|
+
* cancellation, and priority-aware concurrency.
|
|
5
6
|
*/
|
|
6
7
|
export { LifecycleExecutor } from "./lifecycleExecutor.core.js";
|
|
7
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/lifecycle/executor/abandoned
|
|
3
|
+
*
|
|
4
|
+
* Tracks hook invocations that outlived their timeout.
|
|
5
|
+
*/
|
|
6
|
+
import { LifecyclePhase } from "@zudojs/constants";
|
|
7
|
+
/** Predicate over the phase of an abandoned hook. */
|
|
8
|
+
export type PhaseFilter = (phase: LifecyclePhase) => boolean;
|
|
9
|
+
/** True for `stop` and `dispose`. */
|
|
10
|
+
export declare function isShutdownPhase(phase: LifecyclePhase): boolean;
|
|
11
|
+
/** True for `initialize`, `start` and `ready`. */
|
|
12
|
+
export declare function isStartupPhase(phase: LifecyclePhase): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Registry of hooks that are still running after their timeout fired.
|
|
15
|
+
*
|
|
16
|
+
* A timed-out hook cannot be cancelled, only abandoned. It is kept
|
|
17
|
+
* here, per component, so that the component's next hook can wait for
|
|
18
|
+
* it instead of overlapping it (three `listen()` calls on one port).
|
|
19
|
+
*/
|
|
20
|
+
export declare class AbandonedHooks {
|
|
21
|
+
private readonly byComponent;
|
|
22
|
+
/** Starts tracking an invocation; it is forgotten once it settles. */
|
|
23
|
+
track(id: string, phase: LifecyclePhase, invocation: Promise<unknown>): void;
|
|
24
|
+
/** Pending invocations of one component (or all), optionally by phase. */
|
|
25
|
+
pending(id?: string, filter?: PhaseFilter): readonly Promise<unknown>[];
|
|
26
|
+
/**
|
|
27
|
+
* Resolves once the selected invocations have settled, or as soon as
|
|
28
|
+
* `signal` aborts (the shutdown deadline), whichever comes first.
|
|
29
|
+
*/
|
|
30
|
+
settle(id?: string, filter?: PhaseFilter, signal?: AbortSignal): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/lifecycle/executor/abandoned
|
|
3
|
+
*
|
|
4
|
+
* Tracks hook invocations that outlived their timeout.
|
|
5
|
+
*/
|
|
6
|
+
import { LifecyclePhase } from "@zudojs/constants";
|
|
7
|
+
/** True for `stop` and `dispose`. */
|
|
8
|
+
export function isShutdownPhase(phase) {
|
|
9
|
+
return phase === LifecyclePhase.STOP || phase === LifecyclePhase.DISPOSE;
|
|
10
|
+
}
|
|
11
|
+
/** True for `initialize`, `start` and `ready`. */
|
|
12
|
+
export function isStartupPhase(phase) {
|
|
13
|
+
return !isShutdownPhase(phase);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Registry of hooks that are still running after their timeout fired.
|
|
17
|
+
*
|
|
18
|
+
* A timed-out hook cannot be cancelled, only abandoned. It is kept
|
|
19
|
+
* here, per component, so that the component's next hook can wait for
|
|
20
|
+
* it instead of overlapping it (three `listen()` calls on one port).
|
|
21
|
+
*/
|
|
22
|
+
export class AbandonedHooks {
|
|
23
|
+
byComponent = new Map();
|
|
24
|
+
/** Starts tracking an invocation; it is forgotten once it settles. */
|
|
25
|
+
track(id, phase, invocation) {
|
|
26
|
+
const hook = {
|
|
27
|
+
phase,
|
|
28
|
+
settled: invocation.catch(() => undefined),
|
|
29
|
+
};
|
|
30
|
+
const set = this.byComponent.get(id) ?? new Set();
|
|
31
|
+
set.add(hook);
|
|
32
|
+
this.byComponent.set(id, set);
|
|
33
|
+
void hook.settled.finally(() => {
|
|
34
|
+
set.delete(hook);
|
|
35
|
+
if (set.size === 0 && this.byComponent.get(id) === set) {
|
|
36
|
+
this.byComponent.delete(id);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Pending invocations of one component (or all), optionally by phase. */
|
|
41
|
+
pending(id, filter) {
|
|
42
|
+
const sets = id === undefined
|
|
43
|
+
? [...this.byComponent.values()]
|
|
44
|
+
: [this.byComponent.get(id) ?? new Set()];
|
|
45
|
+
return sets
|
|
46
|
+
.flatMap((set) => [...set])
|
|
47
|
+
.filter((hook) => filter?.(hook.phase) ?? true)
|
|
48
|
+
.map((hook) => hook.settled);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves once the selected invocations have settled, or as soon as
|
|
52
|
+
* `signal` aborts (the shutdown deadline), whichever comes first.
|
|
53
|
+
*/
|
|
54
|
+
async settle(id, filter, signal) {
|
|
55
|
+
for (;;) {
|
|
56
|
+
const pending = this.pending(id, filter);
|
|
57
|
+
if (pending.length === 0 || signal?.aborted)
|
|
58
|
+
return;
|
|
59
|
+
await raceAbort(Promise.allSettled(pending), signal);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function raceAbort(work, signal) {
|
|
64
|
+
if (signal === undefined) {
|
|
65
|
+
return work.then(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const done = () => {
|
|
69
|
+
signal.removeEventListener("abort", done);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
signal.addEventListener("abort", done, { once: true });
|
|
73
|
+
void work.then(done, done);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -6,32 +6,28 @@
|
|
|
6
6
|
import type { LifecyclePhase } from "@zudojs/constants";
|
|
7
7
|
import type { LifecycleRegistration } from "../lifecycleComponent/lifecycleComponent.type.js";
|
|
8
8
|
import type { LifecycleContext } from "../lifecycleContext/lifecycleContext.type.js";
|
|
9
|
-
|
|
10
|
-
export interface ExecutionResult {
|
|
11
|
-
/** Component ID. */
|
|
12
|
-
readonly id: string;
|
|
13
|
-
/** The phase that was executed. */
|
|
14
|
-
readonly phase: LifecyclePhase;
|
|
15
|
-
/** Duration in ms. */
|
|
16
|
-
readonly duration: number;
|
|
17
|
-
/** Error if the hook failed. */
|
|
18
|
-
readonly error?: unknown;
|
|
19
|
-
/** Whether the operation succeeded. */
|
|
20
|
-
readonly success: boolean;
|
|
21
|
-
}
|
|
9
|
+
import type { ExecutionResult, LifecycleExecutorOptions } from "./lifecycleExecutor.type.js";
|
|
22
10
|
/**
|
|
23
11
|
* Executes lifecycle component hooks with timeout, retry, and concurrency support.
|
|
12
|
+
*
|
|
13
|
+
* Every invocation gets its own AbortSignal, derived from the run
|
|
14
|
+
* signal and aborted when the component's `timeout` elapses, so a hook
|
|
15
|
+
* that honours `context.signal` unwinds promptly. One that ignores it
|
|
16
|
+
* keeps running and is tracked per component: before that component's
|
|
17
|
+
* `stop()`/`dispose()` the executor waits for it — a drain that overran
|
|
18
|
+
* its stop timeout is waited for until the global deadline, a startup
|
|
19
|
+
* hook that ignored its timeout only for one more `timeout` — so hooks
|
|
20
|
+
* of one component never overlap.
|
|
24
21
|
*/
|
|
25
22
|
export declare class LifecycleExecutor {
|
|
26
|
-
/** Hook invocations still running after their timeout fired. */
|
|
27
23
|
private readonly abandoned;
|
|
24
|
+
private readonly onRetry;
|
|
25
|
+
constructor(options?: LifecycleExecutorOptions);
|
|
28
26
|
/**
|
|
29
|
-
* Resolves once every hook abandoned by a timeout has settled
|
|
30
|
-
*
|
|
31
|
-
* Shutdown waits on this before stopping components, so `stop()`
|
|
32
|
-
* never overlaps a `start()` that is still running.
|
|
27
|
+
* Resolves once every hook abandoned by a timeout has settled — those
|
|
28
|
+
* of one component when `id` is given, otherwise all of them.
|
|
33
29
|
*/
|
|
34
|
-
settleAbandoned(): Promise<void>;
|
|
30
|
+
settleAbandoned(id?: string): Promise<void>;
|
|
35
31
|
/**
|
|
36
32
|
* Executes a single component hook.
|
|
37
33
|
*/
|
|
@@ -42,11 +38,9 @@ export declare class LifecycleExecutor {
|
|
|
42
38
|
* The stage arrives already ordered by priority (descending for
|
|
43
39
|
* startup, ascending for shutdown). Components sharing a priority run
|
|
44
40
|
* together, limited by `concurrency`; the next priority group only
|
|
45
|
-
* begins once the previous one has settled.
|
|
46
|
-
* concurrently made `priority` observable only at `concurrency: 1`,
|
|
47
|
-
* so a `priority: 100` component documented as starting first lost
|
|
48
|
-
* the race to any sibling with a faster hook.
|
|
41
|
+
* begins once the previous one has settled.
|
|
49
42
|
*/
|
|
50
43
|
executeStage(registrations: readonly LifecycleRegistration[], phase: LifecyclePhase, context: LifecycleContext, concurrency: number): Promise<readonly ExecutionResult[]>;
|
|
44
|
+
private invoke;
|
|
45
|
+
private notifyRetry;
|
|
51
46
|
}
|
|
52
|
-
//# sourceMappingURL=lifecycleExecutor.core.d.ts.map
|