@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.
Files changed (70) hide show
  1. package/README.md +44 -15
  2. package/dist/index.d.ts +2 -3
  3. package/dist/index.js +7 -1
  4. package/dist/lifecycleComponent/index.d.ts +0 -1
  5. package/dist/lifecycleComponent/index.js +0 -1
  6. package/dist/lifecycleComponent/lifecycleComponent.type.d.ts +27 -8
  7. package/dist/lifecycleComponent/lifecycleComponent.type.js +0 -1
  8. package/dist/lifecycleContext/index.d.ts +0 -1
  9. package/dist/lifecycleContext/index.js +0 -1
  10. package/dist/lifecycleContext/lifecycleContext.type.d.ts +0 -1
  11. package/dist/lifecycleContext/lifecycleContext.type.js +0 -1
  12. package/dist/lifecycleEvents/index.d.ts +0 -1
  13. package/dist/lifecycleEvents/index.js +0 -1
  14. package/dist/lifecycleEvents/lifecycleEvents.core.d.ts +20 -5
  15. package/dist/lifecycleEvents/lifecycleEvents.core.js +0 -1
  16. package/dist/lifecycleExecutor/index.d.ts +3 -3
  17. package/dist/lifecycleExecutor/index.js +2 -2
  18. package/dist/lifecycleExecutor/lifecycleExecutor.abandoned.d.ts +31 -0
  19. package/dist/lifecycleExecutor/lifecycleExecutor.abandoned.js +75 -0
  20. package/dist/lifecycleExecutor/lifecycleExecutor.core.d.ts +18 -24
  21. package/dist/lifecycleExecutor/lifecycleExecutor.core.js +64 -105
  22. package/dist/lifecycleExecutor/lifecycleExecutor.retry.d.ts +30 -0
  23. package/dist/lifecycleExecutor/lifecycleExecutor.retry.js +66 -0
  24. package/dist/lifecycleExecutor/lifecycleExecutor.type.d.ts +50 -0
  25. package/dist/lifecycleExecutor/lifecycleExecutor.type.js +6 -0
  26. package/dist/lifecycleInternal/asyncUtils.core.d.ts +0 -1
  27. package/dist/lifecycleInternal/asyncUtils.core.js +0 -1
  28. package/dist/lifecycleInternal/dependencyGraph.core.d.ts +34 -7
  29. package/dist/lifecycleInternal/dependencyGraph.core.js +55 -38
  30. package/dist/lifecycleInternal/dependencyGraph.cycle.d.ts +20 -0
  31. package/dist/lifecycleInternal/dependencyGraph.cycle.js +57 -0
  32. package/dist/lifecycleInternal/index.d.ts +1 -1
  33. package/dist/lifecycleInternal/index.js +0 -1
  34. package/dist/lifecycleInternal/timeoutBudget.core.d.ts +0 -1
  35. package/dist/lifecycleInternal/timeoutBudget.core.js +0 -1
  36. package/dist/lifecycleInternal/topologicalSort.core.d.ts +12 -2
  37. package/dist/lifecycleInternal/topologicalSort.core.js +41 -22
  38. package/dist/lifecycleManager/index.d.ts +0 -1
  39. package/dist/lifecycleManager/index.js +0 -1
  40. package/dist/lifecycleManager/lifecycleManager.context.d.ts +15 -7
  41. package/dist/lifecycleManager/lifecycleManager.context.js +0 -1
  42. package/dist/lifecycleManager/lifecycleManager.core.d.ts +12 -2
  43. package/dist/lifecycleManager/lifecycleManager.core.js +30 -3
  44. package/dist/lifecycleManager/lifecycleManager.deadline.d.ts +29 -0
  45. package/dist/lifecycleManager/lifecycleManager.deadline.js +79 -0
  46. package/dist/lifecycleManager/lifecycleManager.shutdown.d.ts +0 -1
  47. package/dist/lifecycleManager/lifecycleManager.shutdown.js +60 -69
  48. package/dist/lifecycleManager/lifecycleManager.startup.d.ts +0 -1
  49. package/dist/lifecycleManager/lifecycleManager.startup.js +12 -4
  50. package/dist/lifecyclePhase/index.d.ts +0 -1
  51. package/dist/lifecyclePhase/index.js +0 -1
  52. package/dist/lifecyclePhase/lifecyclePhase.core.d.ts +0 -1
  53. package/dist/lifecyclePhase/lifecyclePhase.core.js +0 -1
  54. package/dist/lifecyclePlan/index.d.ts +0 -1
  55. package/dist/lifecyclePlan/index.js +0 -1
  56. package/dist/lifecyclePlan/lifecyclePlan.core.d.ts +0 -1
  57. package/dist/lifecyclePlan/lifecyclePlan.core.js +0 -2
  58. package/dist/lifecycleRegistry/index.d.ts +0 -1
  59. package/dist/lifecycleRegistry/index.js +0 -1
  60. package/dist/lifecycleRegistry/lifecycleRegistry.core.d.ts +0 -1
  61. package/dist/lifecycleRegistry/lifecycleRegistry.core.js +3 -2
  62. package/dist/lifecycleSignal/index.d.ts +0 -1
  63. package/dist/lifecycleSignal/index.js +0 -1
  64. package/dist/lifecycleSignal/lifecycleSignal.handler.d.ts +0 -1
  65. package/dist/lifecycleSignal/lifecycleSignal.handler.js +0 -1
  66. package/dist/lifecycleState/index.d.ts +0 -1
  67. package/dist/lifecycleState/index.js +0 -1
  68. package/dist/lifecycleState/lifecycleState.machine.d.ts +0 -1
  69. package/dist/lifecycleState/lifecycleState.machine.js +0 -1
  70. 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 ran: `stop()` is called on components
64
- whose `start` phase ran, and `dispose()` on components whose
65
- `initialize` phase ran.
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 shared by the
90
- whole run and is aborted when the shutdown deadline expires. Long-running
91
- hooks should honour it:
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:ready`,
105
- `component:stopping`, `component:stopped`, `component:failed`
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:ready`, `application:stopping`,
108
- `application:stopped`, `application:disposed`
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` covers hooks that fail; a hook that times out is not retried,
138
- because it is still running and a second call would overlap it.
139
- `shutdown()` waits (within its deadline) for such an abandoned hook to
140
- settle before calling `stop()`.
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
@@ -4,4 +4,3 @@
4
4
  * Lifecycle component interface and registration types.
5
5
  */
6
6
  export type { LifecycleComponent, LifecycleRegistrationOptions, LifecycleRetryOptions, LifecycleRegistration, } from "./lifecycleComponent.type.js";
7
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * Lifecycle component interface and registration types.
5
5
  */
6
6
  export {};
7
- //# 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 individual component operations. `Infinity` means
44
- * no bound; NaN and negative values are rejected at registration.
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
- /** Retry options for component operations. */
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
- /** Maximum number of retry attempts. */
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 between retries in ms. */
71
+ /** Delay before the first retry in ms. Defaults to 500. */
55
72
  readonly delay?: number;
56
- /** Maximum delay between retries in ms. */
73
+ /** Cap on the delay between retries in ms. Defaults to 10 000. */
57
74
  readonly maxDelay?: number;
58
- /** Backoff strategy. */
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
@@ -4,4 +4,3 @@
4
4
  * Lifecycle component interface — the contract for managed resources.
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=lifecycleComponent.type.js.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export type { LifecycleContext } from "./lifecycleContext.type.js";
7
7
  export { createLifecycleContext } from "./lifecycleContext.type.js";
8
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * Lifecycle context type and factory.
5
5
  */
6
6
  export { createLifecycleContext } from "./lifecycleContext.type.js";
7
- //# sourceMappingURL=index.js.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
@@ -12,4 +12,3 @@ export function createLifecycleContext(phase, startedAt, signal, metadata) {
12
12
  metadata: new Map(Object.entries(metadata ?? {})),
13
13
  };
14
14
  }
15
- //# sourceMappingURL=lifecycleContext.type.js.map
@@ -5,4 +5,3 @@
5
5
  */
6
6
  export { LifecycleEventEmitter } from "./lifecycleEvents.core.js";
7
7
  export type { LifecycleEventType, LifecycleComponentEvent, LifecycleApplicationEvent, LifecycleEvent, LifecycleEventListener, } from "./lifecycleEvents.core.js";
8
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * Lifecycle event emitter for observability integration.
5
5
  */
6
6
  export { LifecycleEventEmitter } from "./lifecycleEvents.core.js";
7
- //# sourceMappingURL=index.js.map
@@ -3,16 +3,28 @@
3
3
  *
4
4
  * Lifecycle event emitter — emits typed events for observability integration.
5
5
  */
6
- /** Lifecycle event types. */
7
- export type LifecycleEventType = "component:registered" | "component:initializing" | "component:initialized" | "component:starting" | "component:started" | "component:ready" | "component:stopping" | "component:stopped" | "component:failed" | "application:initializing" | "application:initialized" | "application:starting" | "application:ready" | "application:stopping" | "application:stopped" | "application:disposed";
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
- /** Error if the event represents a failure. */
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
@@ -43,4 +43,3 @@ export class LifecycleEventEmitter {
43
43
  this._listeners.clear();
44
44
  }
45
45
  }
46
- //# sourceMappingURL=lifecycleEvents.core.js.map
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @zudojs/lifecycle/executor
3
3
  *
4
- * Lifecycle executor — runs component hooks with timeout, retry, and concurrency.
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.core.js";
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
- * Lifecycle executor — runs component hooks with timeout, retry, and concurrency.
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
- /** Result of executing a component hook. */
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. Launching the whole stage
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