lifecycleion 0.0.13 → 0.0.14

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.
@@ -158,7 +158,7 @@ interface RestartComponentOptions {
158
158
  /**
159
159
  * Stable, machine-readable failure codes for individual component operations
160
160
  */
161
- type ComponentOperationFailureCode = 'component_not_found' | 'component_already_running' | 'component_already_starting' | 'component_already_stopping' | 'component_not_running' | 'component_stalled' | 'missing_dependency' | 'dependency_not_running' | 'has_running_dependents' | 'startup_in_progress' | 'shutdown_in_progress' | 'component_startup_timeout' | 'component_shutdown_timeout' | 'restart_stop_failed' | 'restart_start_failed' | 'unknown_error';
161
+ type ComponentOperationFailureCode = 'component_not_found' | 'component_already_running' | 'component_already_starting' | 'component_already_stopping' | 'component_not_running' | 'component_stalled' | 'missing_dependency' | 'dependency_not_running' | 'has_running_dependents' | 'startup_in_progress' | 'shutdown_in_progress' | 'component_unexpected_stop' | 'component_startup_timeout' | 'component_shutdown_timeout' | 'restart_stop_failed' | 'restart_start_failed' | 'unknown_error';
162
162
  /**
163
163
  * Failure codes for unregister operations
164
164
  */
@@ -202,7 +202,7 @@ interface StartupResult {
202
202
  /** Reason for failure (when success is false) */
203
203
  reason?: string;
204
204
  /** Error code (when success is false) */
205
- code?: 'already_in_progress' | 'shutdown_in_progress' | 'dependency_cycle' | 'no_components_registered' | 'stalled_components_exist' | 'partial_state' | 'required_component_failed' | 'startup_timeout' | 'unknown_error';
205
+ code?: 'already_in_progress' | 'component_unexpected_stop' | 'shutdown_in_progress' | 'dependency_cycle' | 'no_components_registered' | 'stalled_components_exist' | 'partial_state' | 'required_component_failed' | 'startup_timeout' | 'unknown_error';
206
206
  /** Error object (when success is false due to dependency cycle or unknown error) */
207
207
  error?: Error;
208
208
  /** Total startup duration in milliseconds */
@@ -956,6 +956,10 @@ declare abstract class BaseComponent {
956
956
  protected name: string;
957
957
  /** Reference to component-scoped lifecycle (set by manager when registered) */
958
958
  protected lifecycle: ComponentLifecycleRef;
959
+ /** @internal Set by LifecycleManager while the component is running. */
960
+ private _unexpectedStopHandler?;
961
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
962
+ private _unexpectedStopGeneration;
959
963
  /**
960
964
  * Create a new component
961
965
  *
@@ -964,6 +968,10 @@ declare abstract class BaseComponent {
964
968
  * @throws {InvalidComponentNameError} If name doesn't match kebab-case pattern
965
969
  */
966
970
  constructor(rootLogger: Logger, options: ComponentOptions);
971
+ /** @internal Called by LifecycleManager after a successful start. */
972
+ _setUnexpectedStopHandler(handler: (error?: Error) => boolean): void;
973
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
974
+ _clearUnexpectedStopHandler(): void;
967
975
  /**
968
976
  * Start the component
969
977
  *
@@ -1146,6 +1154,28 @@ declare abstract class BaseComponent {
1146
1154
  * Check if component is optional
1147
1155
  */
1148
1156
  isOptional(): boolean;
1157
+ /**
1158
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
1159
+ * successful start so captured references from older runs go stale.
1160
+ */
1161
+ protected reportUnexpectedStop: (error?: Error) => boolean;
1162
+ /**
1163
+ * Get this component's own status from the manager's perspective.
1164
+ *
1165
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
1166
+ * needing to pass the name. Returns `undefined` if the component is not registered.
1167
+ *
1168
+ * Check `status?.state === 'running'` to test whether the component is currently running.
1169
+ */
1170
+ protected getSelfStatus(): ComponentStatus | undefined;
1171
+ /**
1172
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
1173
+ *
1174
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
1175
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
1176
+ * from a previous run from stopping a newer run of the same component instance.
1177
+ */
1178
+ protected getUnexpectedStopReporter(): (error?: Error) => boolean;
1149
1179
  }
1150
1180
 
1151
1181
  /**
@@ -1179,7 +1209,11 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1179
1209
  private componentTimestamps;
1180
1210
  private componentErrors;
1181
1211
  private componentStartAttemptTokens;
1212
+ private componentStopAttemptTokens;
1213
+ private pendingForceStopWaiters;
1214
+ private unexpectedStopsDuringStartup;
1182
1215
  private isStarting;
1216
+ private autoAttachedSignalsDuringStartup;
1183
1217
  private isStarted;
1184
1218
  private isShuttingDown;
1185
1219
  private shutdownToken;
@@ -1592,6 +1626,45 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1592
1626
  private autoAttachSignals;
1593
1627
  private autoDetachSignalsIfIdle;
1594
1628
  private monitorLateStartupCompletion;
1629
+ private consumeUnexpectedStopsDuringStartup;
1630
+ /**
1631
+ * Issues and returns a unique stop attempt token for a component.
1632
+ *
1633
+ * Each stop attempt (graceful or force-retry) gets a unique token.
1634
+ * The late-resolution handler captures this token in its closure so it can
1635
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
1636
+ * force-retry that also timed out after the original graceful promise floated
1637
+ * in the background.
1638
+ */
1639
+ private issueStopAttemptToken;
1640
+ private createPendingForceStopWaiter;
1641
+ private resolvePendingForceStopWaiters;
1642
+ /**
1643
+ * Called when a stop promise eventually resolves after its timeout path already fired.
1644
+ *
1645
+ * Usually this means a previously stalled component's original stop() or
1646
+ * onShutdownForce() promise finally resolved, so the manager can clear the
1647
+ * stall and transition the component to stopped without a manual retry.
1648
+ *
1649
+ * There is one extra overlap case for graceful stop(): stop() can resolve
1650
+ * after the graceful timeout but before onShutdownForce() itself times out.
1651
+ * In that window no stall entry exists yet, but the component still finished
1652
+ * stopping cleanly, so we finalize it here and let the later force-timeout
1653
+ * path observe the already-stopped state and no-op. This overlap fix is
1654
+ * scoped to the same stop token and will not cross a later retry attempt.
1655
+ *
1656
+ * Two guards prevent stale floating promises from incorrectly clearing state:
1657
+ *
1658
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
1659
+ * force-retry) has started since this promise was launched, its token
1660
+ * won't match and we bail out immediately.
1661
+ *
1662
+ * 2. state/stall guard — if the component was unregistered, restarted, or
1663
+ * already cleared by another path, there will be neither a matching stall
1664
+ * entry nor the force-phase overlap state, so we bail out.
1665
+ */
1666
+ private handleLateStopResolution;
1667
+ private handleComponentUnexpectedStop;
1595
1668
  /**
1596
1669
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
1597
1670
  */
@@ -1713,36 +1786,27 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1713
1786
  */
1714
1787
  private armRepeatedShutdownAfterFailure;
1715
1788
  /**
1716
- * Handle reload request - calls custom callback or broadcasts to components.
1789
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
1790
+ * emits the signal event, then either invokes the user-supplied callback
1791
+ * (passing the broadcast function so the user controls when/whether to
1792
+ * broadcast) or broadcasts directly when no callback is configured.
1717
1793
  *
1718
1794
  * When called from signal handlers (source='signal'), the Promise is started
1719
- * but not awaited due to Node.js signal handler constraints. Components are
1720
- * still notified and the work completes, but return values are not accessible.
1721
- *
1795
+ * but not awaited Node.js signal handlers cannot return values, so results
1796
+ * are not accessible. Components are still notified and the work completes.
1722
1797
  * When called from manual triggers (source='trigger'), the Promise is awaited
1723
1798
  * and results are returned for programmatic use.
1724
- *
1725
- * @param source - Whether triggered from signal manager or manual trigger
1726
1799
  */
1800
+ private handleSignalRequest;
1727
1801
  private handleReloadRequest;
1728
- /**
1729
- * Handle info request - calls custom callback or broadcasts to components.
1730
- *
1731
- * When called from signal handlers, the Promise executes but return values
1732
- * are not accessible due to Node.js signal handler constraints.
1733
- *
1734
- * @param source - Whether triggered from signal manager or manual trigger
1735
- */
1736
1802
  private handleInfoRequest;
1803
+ private handleDebugRequest;
1737
1804
  /**
1738
- * Handle debug request - calls custom callback or broadcasts to components.
1739
- *
1740
- * When called from signal handlers, the Promise executes but return values
1741
- * are not accessible due to Node.js signal handler constraints.
1742
- *
1743
- * @param source - Whether triggered from signal manager or manual trigger
1805
+ * Shared signal broadcast pipeline used by reload/info/debug.
1806
+ * Iterates running components, runs the picked handler with timeout, and
1807
+ * aggregates per-component results into a SignalBroadcastResult.
1744
1808
  */
1745
- private handleDebugRequest;
1809
+ private runSignalBroadcast;
1746
1810
  /**
1747
1811
  * Broadcast reload signal to all running components.
1748
1812
  * Calls onReload() on components that implement it.
@@ -1962,6 +2026,15 @@ interface LifecycleManagerEventMap {
1962
2026
  reason?: string;
1963
2027
  code?: string;
1964
2028
  };
2029
+ 'component:stalled-resolved': {
2030
+ name: string;
2031
+ stallInfo: ComponentStallInfo;
2032
+ stalledDurationMS: number;
2033
+ };
2034
+ 'component:unexpected-stop': {
2035
+ name: string;
2036
+ error?: Error;
2037
+ };
1965
2038
  'component:shutdown-force-completed': {
1966
2039
  name: string;
1967
2040
  };
@@ -2140,6 +2213,8 @@ declare class LifecycleManagerEvents {
2140
2213
  reason?: string;
2141
2214
  code?: string;
2142
2215
  }): void;
2216
+ componentStalledResolved(name: string, stallInfo: ComponentStallInfo, stalledDurationMS: number): void;
2217
+ componentUnexpectedStop(name: string, error?: Error): void;
2143
2218
  componentShutdownForceCompleted(name: string): void;
2144
2219
  componentShutdownForceTimeout(name: string, timeoutMS: number): void;
2145
2220
  componentStartupRollback(name: string): void;
@@ -158,7 +158,7 @@ interface RestartComponentOptions {
158
158
  /**
159
159
  * Stable, machine-readable failure codes for individual component operations
160
160
  */
161
- type ComponentOperationFailureCode = 'component_not_found' | 'component_already_running' | 'component_already_starting' | 'component_already_stopping' | 'component_not_running' | 'component_stalled' | 'missing_dependency' | 'dependency_not_running' | 'has_running_dependents' | 'startup_in_progress' | 'shutdown_in_progress' | 'component_startup_timeout' | 'component_shutdown_timeout' | 'restart_stop_failed' | 'restart_start_failed' | 'unknown_error';
161
+ type ComponentOperationFailureCode = 'component_not_found' | 'component_already_running' | 'component_already_starting' | 'component_already_stopping' | 'component_not_running' | 'component_stalled' | 'missing_dependency' | 'dependency_not_running' | 'has_running_dependents' | 'startup_in_progress' | 'shutdown_in_progress' | 'component_unexpected_stop' | 'component_startup_timeout' | 'component_shutdown_timeout' | 'restart_stop_failed' | 'restart_start_failed' | 'unknown_error';
162
162
  /**
163
163
  * Failure codes for unregister operations
164
164
  */
@@ -202,7 +202,7 @@ interface StartupResult {
202
202
  /** Reason for failure (when success is false) */
203
203
  reason?: string;
204
204
  /** Error code (when success is false) */
205
- code?: 'already_in_progress' | 'shutdown_in_progress' | 'dependency_cycle' | 'no_components_registered' | 'stalled_components_exist' | 'partial_state' | 'required_component_failed' | 'startup_timeout' | 'unknown_error';
205
+ code?: 'already_in_progress' | 'component_unexpected_stop' | 'shutdown_in_progress' | 'dependency_cycle' | 'no_components_registered' | 'stalled_components_exist' | 'partial_state' | 'required_component_failed' | 'startup_timeout' | 'unknown_error';
206
206
  /** Error object (when success is false due to dependency cycle or unknown error) */
207
207
  error?: Error;
208
208
  /** Total startup duration in milliseconds */
@@ -956,6 +956,10 @@ declare abstract class BaseComponent {
956
956
  protected name: string;
957
957
  /** Reference to component-scoped lifecycle (set by manager when registered) */
958
958
  protected lifecycle: ComponentLifecycleRef;
959
+ /** @internal Set by LifecycleManager while the component is running. */
960
+ private _unexpectedStopHandler?;
961
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
962
+ private _unexpectedStopGeneration;
959
963
  /**
960
964
  * Create a new component
961
965
  *
@@ -964,6 +968,10 @@ declare abstract class BaseComponent {
964
968
  * @throws {InvalidComponentNameError} If name doesn't match kebab-case pattern
965
969
  */
966
970
  constructor(rootLogger: Logger, options: ComponentOptions);
971
+ /** @internal Called by LifecycleManager after a successful start. */
972
+ _setUnexpectedStopHandler(handler: (error?: Error) => boolean): void;
973
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
974
+ _clearUnexpectedStopHandler(): void;
967
975
  /**
968
976
  * Start the component
969
977
  *
@@ -1146,6 +1154,28 @@ declare abstract class BaseComponent {
1146
1154
  * Check if component is optional
1147
1155
  */
1148
1156
  isOptional(): boolean;
1157
+ /**
1158
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
1159
+ * successful start so captured references from older runs go stale.
1160
+ */
1161
+ protected reportUnexpectedStop: (error?: Error) => boolean;
1162
+ /**
1163
+ * Get this component's own status from the manager's perspective.
1164
+ *
1165
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
1166
+ * needing to pass the name. Returns `undefined` if the component is not registered.
1167
+ *
1168
+ * Check `status?.state === 'running'` to test whether the component is currently running.
1169
+ */
1170
+ protected getSelfStatus(): ComponentStatus | undefined;
1171
+ /**
1172
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
1173
+ *
1174
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
1175
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
1176
+ * from a previous run from stopping a newer run of the same component instance.
1177
+ */
1178
+ protected getUnexpectedStopReporter(): (error?: Error) => boolean;
1149
1179
  }
1150
1180
 
1151
1181
  /**
@@ -1179,7 +1209,11 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1179
1209
  private componentTimestamps;
1180
1210
  private componentErrors;
1181
1211
  private componentStartAttemptTokens;
1212
+ private componentStopAttemptTokens;
1213
+ private pendingForceStopWaiters;
1214
+ private unexpectedStopsDuringStartup;
1182
1215
  private isStarting;
1216
+ private autoAttachedSignalsDuringStartup;
1183
1217
  private isStarted;
1184
1218
  private isShuttingDown;
1185
1219
  private shutdownToken;
@@ -1592,6 +1626,45 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1592
1626
  private autoAttachSignals;
1593
1627
  private autoDetachSignalsIfIdle;
1594
1628
  private monitorLateStartupCompletion;
1629
+ private consumeUnexpectedStopsDuringStartup;
1630
+ /**
1631
+ * Issues and returns a unique stop attempt token for a component.
1632
+ *
1633
+ * Each stop attempt (graceful or force-retry) gets a unique token.
1634
+ * The late-resolution handler captures this token in its closure so it can
1635
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
1636
+ * force-retry that also timed out after the original graceful promise floated
1637
+ * in the background.
1638
+ */
1639
+ private issueStopAttemptToken;
1640
+ private createPendingForceStopWaiter;
1641
+ private resolvePendingForceStopWaiters;
1642
+ /**
1643
+ * Called when a stop promise eventually resolves after its timeout path already fired.
1644
+ *
1645
+ * Usually this means a previously stalled component's original stop() or
1646
+ * onShutdownForce() promise finally resolved, so the manager can clear the
1647
+ * stall and transition the component to stopped without a manual retry.
1648
+ *
1649
+ * There is one extra overlap case for graceful stop(): stop() can resolve
1650
+ * after the graceful timeout but before onShutdownForce() itself times out.
1651
+ * In that window no stall entry exists yet, but the component still finished
1652
+ * stopping cleanly, so we finalize it here and let the later force-timeout
1653
+ * path observe the already-stopped state and no-op. This overlap fix is
1654
+ * scoped to the same stop token and will not cross a later retry attempt.
1655
+ *
1656
+ * Two guards prevent stale floating promises from incorrectly clearing state:
1657
+ *
1658
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
1659
+ * force-retry) has started since this promise was launched, its token
1660
+ * won't match and we bail out immediately.
1661
+ *
1662
+ * 2. state/stall guard — if the component was unregistered, restarted, or
1663
+ * already cleared by another path, there will be neither a matching stall
1664
+ * entry nor the force-phase overlap state, so we bail out.
1665
+ */
1666
+ private handleLateStopResolution;
1667
+ private handleComponentUnexpectedStop;
1595
1668
  /**
1596
1669
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
1597
1670
  */
@@ -1713,36 +1786,27 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1713
1786
  */
1714
1787
  private armRepeatedShutdownAfterFailure;
1715
1788
  /**
1716
- * Handle reload request - calls custom callback or broadcasts to components.
1789
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
1790
+ * emits the signal event, then either invokes the user-supplied callback
1791
+ * (passing the broadcast function so the user controls when/whether to
1792
+ * broadcast) or broadcasts directly when no callback is configured.
1717
1793
  *
1718
1794
  * When called from signal handlers (source='signal'), the Promise is started
1719
- * but not awaited due to Node.js signal handler constraints. Components are
1720
- * still notified and the work completes, but return values are not accessible.
1721
- *
1795
+ * but not awaited Node.js signal handlers cannot return values, so results
1796
+ * are not accessible. Components are still notified and the work completes.
1722
1797
  * When called from manual triggers (source='trigger'), the Promise is awaited
1723
1798
  * and results are returned for programmatic use.
1724
- *
1725
- * @param source - Whether triggered from signal manager or manual trigger
1726
1799
  */
1800
+ private handleSignalRequest;
1727
1801
  private handleReloadRequest;
1728
- /**
1729
- * Handle info request - calls custom callback or broadcasts to components.
1730
- *
1731
- * When called from signal handlers, the Promise executes but return values
1732
- * are not accessible due to Node.js signal handler constraints.
1733
- *
1734
- * @param source - Whether triggered from signal manager or manual trigger
1735
- */
1736
1802
  private handleInfoRequest;
1803
+ private handleDebugRequest;
1737
1804
  /**
1738
- * Handle debug request - calls custom callback or broadcasts to components.
1739
- *
1740
- * When called from signal handlers, the Promise executes but return values
1741
- * are not accessible due to Node.js signal handler constraints.
1742
- *
1743
- * @param source - Whether triggered from signal manager or manual trigger
1805
+ * Shared signal broadcast pipeline used by reload/info/debug.
1806
+ * Iterates running components, runs the picked handler with timeout, and
1807
+ * aggregates per-component results into a SignalBroadcastResult.
1744
1808
  */
1745
- private handleDebugRequest;
1809
+ private runSignalBroadcast;
1746
1810
  /**
1747
1811
  * Broadcast reload signal to all running components.
1748
1812
  * Calls onReload() on components that implement it.
@@ -1962,6 +2026,15 @@ interface LifecycleManagerEventMap {
1962
2026
  reason?: string;
1963
2027
  code?: string;
1964
2028
  };
2029
+ 'component:stalled-resolved': {
2030
+ name: string;
2031
+ stallInfo: ComponentStallInfo;
2032
+ stalledDurationMS: number;
2033
+ };
2034
+ 'component:unexpected-stop': {
2035
+ name: string;
2036
+ error?: Error;
2037
+ };
1965
2038
  'component:shutdown-force-completed': {
1966
2039
  name: string;
1967
2040
  };
@@ -2140,6 +2213,8 @@ declare class LifecycleManagerEvents {
2140
2213
  reason?: string;
2141
2214
  code?: string;
2142
2215
  }): void;
2216
+ componentStalledResolved(name: string, stallInfo: ComponentStallInfo, stalledDurationMS: number): void;
2217
+ componentUnexpectedStop(name: string, error?: Error): void;
2143
2218
  componentShutdownForceCompleted(name: string): void;
2144
2219
  componentShutdownForceTimeout(name: string, timeoutMS: number): void;
2145
2220
  componentStartupRollback(name: string): void;