lifecycleion 0.0.2 → 0.0.4

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.
@@ -697,7 +697,9 @@ interface LifecycleManagerOptions {
697
697
  shutdownWarningTimeoutMS?: number;
698
698
  /** Default message timeout in ms (default: 5000, 0 = disabled) */
699
699
  messageTimeoutMS?: number;
700
- /** Auto-attach signals when first component starts (default: false) */
700
+ /** Auto-attach signals before startAllComponents()/startComponent() begins work, even if startup later fails (default: false) */
701
+ attachSignalsBeforeStartup?: boolean;
702
+ /** Auto-attach signals when the first component successfully starts (default: false) */
701
703
  attachSignalsOnStart?: boolean;
702
704
  /** Auto-detach signals when last component stops (default: false) */
703
705
  detachSignalsOnStop?: boolean;
@@ -1003,6 +1005,7 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1003
1005
  private readonly messageTimeoutMS;
1004
1006
  private readonly startupTimeoutMS;
1005
1007
  private readonly shutdownOptions?;
1008
+ private readonly attachSignalsBeforeStartup;
1006
1009
  private readonly attachSignalsOnStart;
1007
1010
  private readonly detachSignalsOnStop;
1008
1011
  private components;
@@ -1011,9 +1014,12 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1011
1014
  private stalledComponents;
1012
1015
  private componentTimestamps;
1013
1016
  private componentErrors;
1017
+ private componentStartAttemptTokens;
1014
1018
  private isStarting;
1015
1019
  private isStarted;
1016
1020
  private isShuttingDown;
1021
+ private shutdownToken;
1022
+ private pendingLoggerExitResolve;
1017
1023
  private shutdownMethod;
1018
1024
  private lastShutdownResult;
1019
1025
  private processSignalManager;
@@ -1340,6 +1346,10 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1340
1346
  */
1341
1347
  private registerComponentInternal;
1342
1348
  private stopAllComponentsInternal;
1349
+ /**
1350
+ * Release a deferred logger.exit() request after shutdown fully settles.
1351
+ */
1352
+ private finalizePendingLoggerExit;
1343
1353
  /**
1344
1354
  * Retry shutdown for a stalled component.
1345
1355
  * Attempts the force phase directly to avoid re-running a failing stop().
@@ -1409,6 +1419,9 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1409
1419
  * Used when a required component fails to start during startAllComponents()
1410
1420
  */
1411
1421
  private rollbackStartup;
1422
+ private autoAttachSignals;
1423
+ private autoDetachSignalsIfIdle;
1424
+ private monitorLateStartupCompletion;
1412
1425
  /**
1413
1426
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
1414
1427
  */
@@ -697,7 +697,9 @@ interface LifecycleManagerOptions {
697
697
  shutdownWarningTimeoutMS?: number;
698
698
  /** Default message timeout in ms (default: 5000, 0 = disabled) */
699
699
  messageTimeoutMS?: number;
700
- /** Auto-attach signals when first component starts (default: false) */
700
+ /** Auto-attach signals before startAllComponents()/startComponent() begins work, even if startup later fails (default: false) */
701
+ attachSignalsBeforeStartup?: boolean;
702
+ /** Auto-attach signals when the first component successfully starts (default: false) */
701
703
  attachSignalsOnStart?: boolean;
702
704
  /** Auto-detach signals when last component stops (default: false) */
703
705
  detachSignalsOnStop?: boolean;
@@ -1003,6 +1005,7 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1003
1005
  private readonly messageTimeoutMS;
1004
1006
  private readonly startupTimeoutMS;
1005
1007
  private readonly shutdownOptions?;
1008
+ private readonly attachSignalsBeforeStartup;
1006
1009
  private readonly attachSignalsOnStart;
1007
1010
  private readonly detachSignalsOnStop;
1008
1011
  private components;
@@ -1011,9 +1014,12 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1011
1014
  private stalledComponents;
1012
1015
  private componentTimestamps;
1013
1016
  private componentErrors;
1017
+ private componentStartAttemptTokens;
1014
1018
  private isStarting;
1015
1019
  private isStarted;
1016
1020
  private isShuttingDown;
1021
+ private shutdownToken;
1022
+ private pendingLoggerExitResolve;
1017
1023
  private shutdownMethod;
1018
1024
  private lastShutdownResult;
1019
1025
  private processSignalManager;
@@ -1340,6 +1346,10 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1340
1346
  */
1341
1347
  private registerComponentInternal;
1342
1348
  private stopAllComponentsInternal;
1349
+ /**
1350
+ * Release a deferred logger.exit() request after shutdown fully settles.
1351
+ */
1352
+ private finalizePendingLoggerExit;
1343
1353
  /**
1344
1354
  * Retry shutdown for a stalled component.
1345
1355
  * Attempts the force phase directly to avoid re-running a failing stop().
@@ -1409,6 +1419,9 @@ declare class LifecycleManager extends EventEmitterProtected implements Lifecycl
1409
1419
  * Used when a required component fails to start during startAllComponents()
1410
1420
  */
1411
1421
  private rollbackStartup;
1422
+ private autoAttachSignals;
1423
+ private autoDetachSignalsIfIdle;
1424
+ private monitorLateStartupCompletion;
1412
1425
  /**
1413
1426
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
1414
1427
  */
@@ -850,6 +850,9 @@ var EventEmitterProtected = class {
850
850
  }
851
851
  };
852
852
 
853
+ // src/lib/lifecycle-manager/lifecycle-manager.ts
854
+ import { ulid as ulid2 } from "ulid";
855
+
853
856
  // src/lib/lifecycle-manager/component-lifecycle.ts
854
857
  var ComponentLifecycle = class {
855
858
  manager;
@@ -1915,6 +1918,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1915
1918
  messageTimeoutMS;
1916
1919
  startupTimeoutMS;
1917
1920
  shutdownOptions;
1921
+ attachSignalsBeforeStartup;
1918
1922
  attachSignalsOnStart;
1919
1923
  detachSignalsOnStop;
1920
1924
  // Component management
@@ -1925,10 +1929,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1925
1929
  // State tracking for individual components
1926
1930
  componentTimestamps = /* @__PURE__ */ new Map();
1927
1931
  componentErrors = /* @__PURE__ */ new Map();
1932
+ componentStartAttemptTokens = /* @__PURE__ */ new Map();
1928
1933
  // State flags
1929
1934
  isStarting = false;
1930
1935
  isStarted = false;
1931
1936
  isShuttingDown = false;
1937
+ // Unique token used to detect shutdowns that happened during async start().
1938
+ shutdownToken = ulid2();
1939
+ // Resolver for the first logger.exit() deferred during an already-running shutdown.
1940
+ pendingLoggerExitResolve = null;
1932
1941
  shutdownMethod = null;
1933
1942
  lastShutdownResult = null;
1934
1943
  // Signal management
@@ -1954,6 +1963,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1954
1963
  haltOnStall: true,
1955
1964
  ...options.shutdownOptions
1956
1965
  };
1966
+ this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
1957
1967
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
1958
1968
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
1959
1969
  this.onReloadRequested = options.onReloadRequested;
@@ -2117,6 +2127,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2117
2127
  this.componentStates.delete(name);
2118
2128
  this.componentTimestamps.delete(name);
2119
2129
  this.componentErrors.delete(name);
2130
+ this.componentStartAttemptTokens.delete(name);
2120
2131
  this.stalledComponents.delete(name);
2121
2132
  this.runningComponents.delete(name);
2122
2133
  this.updateStartedFlag();
@@ -2525,6 +2536,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2525
2536
  this.isStarting = true;
2526
2537
  this.shutdownMethod = null;
2527
2538
  this.lastShutdownResult = null;
2539
+ const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
2528
2540
  this.logger.info("Starting all components");
2529
2541
  const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
2530
2542
  let hasTimedOut = false;
@@ -2638,6 +2650,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2638
2650
  startedComponents.push(name);
2639
2651
  } else if (result.code === "component_already_running") {
2640
2652
  startedComponents.push(name);
2653
+ } else if (result.code === "shutdown_in_progress") {
2654
+ await this.rollbackStartup(startedComponents);
2655
+ return {
2656
+ success: false,
2657
+ startedComponents: [],
2658
+ failedOptionalComponents,
2659
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2660
+ reason: result.reason || "Shutdown triggered during startup",
2661
+ code: "shutdown_in_progress",
2662
+ error: result.error,
2663
+ durationMS: Date.now() - startTime
2664
+ };
2641
2665
  } else {
2642
2666
  if (component.isOptional()) {
2643
2667
  this.logger.entity(name).warn("Optional component failed to start, continuing", {
@@ -2726,6 +2750,9 @@ var LifecycleManager = class extends EventEmitterProtected {
2726
2750
  if (timeoutHandle) {
2727
2751
  clearTimeout(timeoutHandle);
2728
2752
  }
2753
+ if (didAutoAttachSignalsForBulkStartup) {
2754
+ this.autoDetachSignalsIfIdle("failed bulk startup");
2755
+ }
2729
2756
  this.isStarting = false;
2730
2757
  }
2731
2758
  }
@@ -2970,6 +2997,17 @@ var LifecycleManager = class extends EventEmitterProtected {
2970
2997
  this.rootLogger.setBeforeExitCallback(
2971
2998
  async (exitCode, isFirstExit) => {
2972
2999
  if (this.isShuttingDown) {
3000
+ if (isFirstExit && this.pendingLoggerExitResolve === null) {
3001
+ this.logger.debug(
3002
+ "Logger exit called during shutdown, waiting...",
3003
+ {
3004
+ params: { exitCode }
3005
+ }
3006
+ );
3007
+ return await new Promise((resolve) => {
3008
+ this.pendingLoggerExitResolve = resolve;
3009
+ });
3010
+ }
2973
3011
  this.logger.debug("Logger exit called during shutdown, waiting...", {
2974
3012
  params: { exitCode }
2975
3013
  });
@@ -3793,6 +3831,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3793
3831
  stoppedAt: null
3794
3832
  });
3795
3833
  this.componentErrors.set(componentName, null);
3834
+ this.componentStartAttemptTokens.set(componentName, ulid2());
3796
3835
  const isManualPositionRespected = this.isManualPositionRespected({
3797
3836
  componentName,
3798
3837
  position,
@@ -3944,6 +3983,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3944
3983
  };
3945
3984
  }
3946
3985
  this.isShuttingDown = true;
3986
+ this.shutdownToken = ulid2();
3947
3987
  this.shutdownMethod = method;
3948
3988
  const isDuringStartup = this.isStarting;
3949
3989
  this.logger.info("Stopping all components", { params: { method } });
@@ -4073,7 +4113,19 @@ var LifecycleManager = class extends EventEmitterProtected {
4073
4113
  }
4074
4114
  this.isShuttingDown = false;
4075
4115
  this.updateStartedFlag();
4116
+ this.finalizePendingLoggerExit();
4117
+ }
4118
+ }
4119
+ /**
4120
+ * Release a deferred logger.exit() request after shutdown fully settles.
4121
+ */
4122
+ finalizePendingLoggerExit() {
4123
+ if (this.pendingLoggerExitResolve === null || this.isShuttingDown) {
4124
+ return;
4076
4125
  }
4126
+ const resolve = this.pendingLoggerExitResolve;
4127
+ this.pendingLoggerExitResolve = null;
4128
+ resolve({ action: "proceed" });
4077
4129
  }
4078
4130
  /**
4079
4131
  * Retry shutdown for a stalled component.
@@ -4214,6 +4266,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4214
4266
  this.logger.entity(name).info("Starting component");
4215
4267
  this.lifecycleEvents.componentStarting(name);
4216
4268
  const timeoutMS = component.startupTimeoutMS;
4269
+ const startAttemptToken = ulid2();
4270
+ this.componentStartAttemptTokens.set(name, startAttemptToken);
4271
+ const shutdownTokenAtStart = this.shutdownToken;
4272
+ const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4217
4273
  let timeoutHandle;
4218
4274
  try {
4219
4275
  const startPromise = component.start();
@@ -4228,6 +4284,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4228
4284
  params: { error }
4229
4285
  });
4230
4286
  }
4287
+ } else {
4288
+ this.monitorLateStartupCompletion(
4289
+ name,
4290
+ component,
4291
+ startPromise,
4292
+ startAttemptToken
4293
+ );
4231
4294
  }
4232
4295
  Promise.resolve(startPromise).catch(() => {
4233
4296
  });
@@ -4243,15 +4306,36 @@ var LifecycleManager = class extends EventEmitterProtected {
4243
4306
  } else {
4244
4307
  await startPromise;
4245
4308
  }
4309
+ if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4310
+ this.componentStates.set(name, "running");
4311
+ this.runningComponents.add(name);
4312
+ this.stalledComponents.delete(name);
4313
+ this.updateStartedFlag();
4314
+ const timestamps2 = this.componentTimestamps.get(name) ?? {
4315
+ startedAt: null,
4316
+ stoppedAt: null
4317
+ };
4318
+ timestamps2.startedAt = Date.now();
4319
+ this.componentTimestamps.set(name, timestamps2);
4320
+ this.logger.entity(name).warn(
4321
+ "Component finished starting after shutdown began, stopping immediately"
4322
+ );
4323
+ const stopResult = await this.stopComponentInternal(name);
4324
+ return {
4325
+ success: false,
4326
+ componentName: name,
4327
+ reason: "Shutdown triggered during component startup",
4328
+ code: "shutdown_in_progress",
4329
+ error: stopResult.error,
4330
+ status: this.getComponentStatus(name)
4331
+ };
4332
+ }
4246
4333
  this.componentStates.set(name, "running");
4247
4334
  this.runningComponents.add(name);
4248
4335
  this.stalledComponents.delete(name);
4249
4336
  this.updateStartedFlag();
4250
- if (this.attachSignalsOnStart && this.runningComponents.size === 1 && !this.processSignalManager) {
4251
- this.logger.info(
4252
- "Auto-attaching process signals on first component start"
4253
- );
4254
- this.attachSignals();
4337
+ if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4338
+ this.autoAttachSignals("first component start");
4255
4339
  }
4256
4340
  const timestamps = this.componentTimestamps.get(name) ?? {
4257
4341
  startedAt: null,
@@ -4300,6 +4384,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4300
4384
  if (timeoutHandle) {
4301
4385
  clearTimeout(timeoutHandle);
4302
4386
  }
4387
+ if (didAutoAttachSignalsForComponentStartup) {
4388
+ this.autoDetachSignalsIfIdle("failed component startup");
4389
+ }
4303
4390
  }
4304
4391
  }
4305
4392
  /**
@@ -4796,6 +4883,57 @@ var LifecycleManager = class extends EventEmitterProtected {
4796
4883
  }
4797
4884
  this.logger.info("Rollback completed");
4798
4885
  }
4886
+ autoAttachSignals(trigger) {
4887
+ if (this.processSignalManager?.getStatus().isAttached) {
4888
+ return false;
4889
+ }
4890
+ this.logger.info(`Auto-attaching process signals on ${trigger}`);
4891
+ this.attachSignals();
4892
+ return true;
4893
+ }
4894
+ autoDetachSignalsIfIdle(trigger) {
4895
+ if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
4896
+ return;
4897
+ }
4898
+ this.logger.info(`Auto-detaching process signals after ${trigger}`);
4899
+ this.detachSignals();
4900
+ }
4901
+ monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
4902
+ this.logger.entity(name).warn(
4903
+ "Startup timed out without onStartupAborted, stopping component if startup completes later"
4904
+ );
4905
+ Promise.resolve(startPromise).then(async () => {
4906
+ const timeoutState = this.componentStates.get(name);
4907
+ if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
4908
+ return;
4909
+ }
4910
+ this.componentStates.set(name, "running");
4911
+ this.runningComponents.add(name);
4912
+ this.stalledComponents.delete(name);
4913
+ this.updateStartedFlag();
4914
+ const timestamps = this.componentTimestamps.get(name) ?? {
4915
+ startedAt: null,
4916
+ stoppedAt: null
4917
+ };
4918
+ timestamps.startedAt = Date.now();
4919
+ this.componentTimestamps.set(name, timestamps);
4920
+ this.logger.entity(name).warn(
4921
+ "Component completed startup after timeout, stopping automatically"
4922
+ );
4923
+ const stopResult = await this.stopComponentInternal(name);
4924
+ if (!stopResult.success) {
4925
+ this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
4926
+ params: {
4927
+ error: stopResult.error,
4928
+ code: stopResult.code
4929
+ }
4930
+ });
4931
+ return;
4932
+ }
4933
+ this.componentStates.set(name, timeoutState);
4934
+ }).catch(() => {
4935
+ });
4936
+ }
4799
4937
  /**
4800
4938
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
4801
4939
  */