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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Lifecycleion v0.0.2
1
+ # Lifecycleion v0.0.4
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/lifecycleion.svg)](https://badge.fury.io/js/lifecycleion)
4
4
 
@@ -900,6 +900,9 @@ var EventEmitterProtected = class {
900
900
  }
901
901
  };
902
902
 
903
+ // src/lib/lifecycle-manager/lifecycle-manager.ts
904
+ var import_ulid2 = require("ulid");
905
+
903
906
  // src/lib/lifecycle-manager/component-lifecycle.ts
904
907
  var ComponentLifecycle = class {
905
908
  manager;
@@ -1965,6 +1968,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1965
1968
  messageTimeoutMS;
1966
1969
  startupTimeoutMS;
1967
1970
  shutdownOptions;
1971
+ attachSignalsBeforeStartup;
1968
1972
  attachSignalsOnStart;
1969
1973
  detachSignalsOnStop;
1970
1974
  // Component management
@@ -1975,10 +1979,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1975
1979
  // State tracking for individual components
1976
1980
  componentTimestamps = /* @__PURE__ */ new Map();
1977
1981
  componentErrors = /* @__PURE__ */ new Map();
1982
+ componentStartAttemptTokens = /* @__PURE__ */ new Map();
1978
1983
  // State flags
1979
1984
  isStarting = false;
1980
1985
  isStarted = false;
1981
1986
  isShuttingDown = false;
1987
+ // Unique token used to detect shutdowns that happened during async start().
1988
+ shutdownToken = (0, import_ulid2.ulid)();
1989
+ // Resolver for the first logger.exit() deferred during an already-running shutdown.
1990
+ pendingLoggerExitResolve = null;
1982
1991
  shutdownMethod = null;
1983
1992
  lastShutdownResult = null;
1984
1993
  // Signal management
@@ -2004,6 +2013,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2004
2013
  haltOnStall: true,
2005
2014
  ...options.shutdownOptions
2006
2015
  };
2016
+ this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
2007
2017
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
2008
2018
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
2009
2019
  this.onReloadRequested = options.onReloadRequested;
@@ -2167,6 +2177,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2167
2177
  this.componentStates.delete(name);
2168
2178
  this.componentTimestamps.delete(name);
2169
2179
  this.componentErrors.delete(name);
2180
+ this.componentStartAttemptTokens.delete(name);
2170
2181
  this.stalledComponents.delete(name);
2171
2182
  this.runningComponents.delete(name);
2172
2183
  this.updateStartedFlag();
@@ -2575,6 +2586,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2575
2586
  this.isStarting = true;
2576
2587
  this.shutdownMethod = null;
2577
2588
  this.lastShutdownResult = null;
2589
+ const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
2578
2590
  this.logger.info("Starting all components");
2579
2591
  const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
2580
2592
  let hasTimedOut = false;
@@ -2688,6 +2700,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2688
2700
  startedComponents.push(name);
2689
2701
  } else if (result.code === "component_already_running") {
2690
2702
  startedComponents.push(name);
2703
+ } else if (result.code === "shutdown_in_progress") {
2704
+ await this.rollbackStartup(startedComponents);
2705
+ return {
2706
+ success: false,
2707
+ startedComponents: [],
2708
+ failedOptionalComponents,
2709
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2710
+ reason: result.reason || "Shutdown triggered during startup",
2711
+ code: "shutdown_in_progress",
2712
+ error: result.error,
2713
+ durationMS: Date.now() - startTime
2714
+ };
2691
2715
  } else {
2692
2716
  if (component.isOptional()) {
2693
2717
  this.logger.entity(name).warn("Optional component failed to start, continuing", {
@@ -2776,6 +2800,9 @@ var LifecycleManager = class extends EventEmitterProtected {
2776
2800
  if (timeoutHandle) {
2777
2801
  clearTimeout(timeoutHandle);
2778
2802
  }
2803
+ if (didAutoAttachSignalsForBulkStartup) {
2804
+ this.autoDetachSignalsIfIdle("failed bulk startup");
2805
+ }
2779
2806
  this.isStarting = false;
2780
2807
  }
2781
2808
  }
@@ -3020,6 +3047,17 @@ var LifecycleManager = class extends EventEmitterProtected {
3020
3047
  this.rootLogger.setBeforeExitCallback(
3021
3048
  async (exitCode, isFirstExit) => {
3022
3049
  if (this.isShuttingDown) {
3050
+ if (isFirstExit && this.pendingLoggerExitResolve === null) {
3051
+ this.logger.debug(
3052
+ "Logger exit called during shutdown, waiting...",
3053
+ {
3054
+ params: { exitCode }
3055
+ }
3056
+ );
3057
+ return await new Promise((resolve) => {
3058
+ this.pendingLoggerExitResolve = resolve;
3059
+ });
3060
+ }
3023
3061
  this.logger.debug("Logger exit called during shutdown, waiting...", {
3024
3062
  params: { exitCode }
3025
3063
  });
@@ -3843,6 +3881,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3843
3881
  stoppedAt: null
3844
3882
  });
3845
3883
  this.componentErrors.set(componentName, null);
3884
+ this.componentStartAttemptTokens.set(componentName, (0, import_ulid2.ulid)());
3846
3885
  const isManualPositionRespected = this.isManualPositionRespected({
3847
3886
  componentName,
3848
3887
  position,
@@ -3994,6 +4033,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3994
4033
  };
3995
4034
  }
3996
4035
  this.isShuttingDown = true;
4036
+ this.shutdownToken = (0, import_ulid2.ulid)();
3997
4037
  this.shutdownMethod = method;
3998
4038
  const isDuringStartup = this.isStarting;
3999
4039
  this.logger.info("Stopping all components", { params: { method } });
@@ -4123,7 +4163,19 @@ var LifecycleManager = class extends EventEmitterProtected {
4123
4163
  }
4124
4164
  this.isShuttingDown = false;
4125
4165
  this.updateStartedFlag();
4166
+ this.finalizePendingLoggerExit();
4167
+ }
4168
+ }
4169
+ /**
4170
+ * Release a deferred logger.exit() request after shutdown fully settles.
4171
+ */
4172
+ finalizePendingLoggerExit() {
4173
+ if (this.pendingLoggerExitResolve === null || this.isShuttingDown) {
4174
+ return;
4126
4175
  }
4176
+ const resolve = this.pendingLoggerExitResolve;
4177
+ this.pendingLoggerExitResolve = null;
4178
+ resolve({ action: "proceed" });
4127
4179
  }
4128
4180
  /**
4129
4181
  * Retry shutdown for a stalled component.
@@ -4264,6 +4316,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4264
4316
  this.logger.entity(name).info("Starting component");
4265
4317
  this.lifecycleEvents.componentStarting(name);
4266
4318
  const timeoutMS = component.startupTimeoutMS;
4319
+ const startAttemptToken = (0, import_ulid2.ulid)();
4320
+ this.componentStartAttemptTokens.set(name, startAttemptToken);
4321
+ const shutdownTokenAtStart = this.shutdownToken;
4322
+ const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4267
4323
  let timeoutHandle;
4268
4324
  try {
4269
4325
  const startPromise = component.start();
@@ -4278,6 +4334,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4278
4334
  params: { error }
4279
4335
  });
4280
4336
  }
4337
+ } else {
4338
+ this.monitorLateStartupCompletion(
4339
+ name,
4340
+ component,
4341
+ startPromise,
4342
+ startAttemptToken
4343
+ );
4281
4344
  }
4282
4345
  Promise.resolve(startPromise).catch(() => {
4283
4346
  });
@@ -4293,15 +4356,36 @@ var LifecycleManager = class extends EventEmitterProtected {
4293
4356
  } else {
4294
4357
  await startPromise;
4295
4358
  }
4359
+ if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4360
+ this.componentStates.set(name, "running");
4361
+ this.runningComponents.add(name);
4362
+ this.stalledComponents.delete(name);
4363
+ this.updateStartedFlag();
4364
+ const timestamps2 = this.componentTimestamps.get(name) ?? {
4365
+ startedAt: null,
4366
+ stoppedAt: null
4367
+ };
4368
+ timestamps2.startedAt = Date.now();
4369
+ this.componentTimestamps.set(name, timestamps2);
4370
+ this.logger.entity(name).warn(
4371
+ "Component finished starting after shutdown began, stopping immediately"
4372
+ );
4373
+ const stopResult = await this.stopComponentInternal(name);
4374
+ return {
4375
+ success: false,
4376
+ componentName: name,
4377
+ reason: "Shutdown triggered during component startup",
4378
+ code: "shutdown_in_progress",
4379
+ error: stopResult.error,
4380
+ status: this.getComponentStatus(name)
4381
+ };
4382
+ }
4296
4383
  this.componentStates.set(name, "running");
4297
4384
  this.runningComponents.add(name);
4298
4385
  this.stalledComponents.delete(name);
4299
4386
  this.updateStartedFlag();
4300
- if (this.attachSignalsOnStart && this.runningComponents.size === 1 && !this.processSignalManager) {
4301
- this.logger.info(
4302
- "Auto-attaching process signals on first component start"
4303
- );
4304
- this.attachSignals();
4387
+ if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4388
+ this.autoAttachSignals("first component start");
4305
4389
  }
4306
4390
  const timestamps = this.componentTimestamps.get(name) ?? {
4307
4391
  startedAt: null,
@@ -4350,6 +4434,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4350
4434
  if (timeoutHandle) {
4351
4435
  clearTimeout(timeoutHandle);
4352
4436
  }
4437
+ if (didAutoAttachSignalsForComponentStartup) {
4438
+ this.autoDetachSignalsIfIdle("failed component startup");
4439
+ }
4353
4440
  }
4354
4441
  }
4355
4442
  /**
@@ -4846,6 +4933,57 @@ var LifecycleManager = class extends EventEmitterProtected {
4846
4933
  }
4847
4934
  this.logger.info("Rollback completed");
4848
4935
  }
4936
+ autoAttachSignals(trigger) {
4937
+ if (this.processSignalManager?.getStatus().isAttached) {
4938
+ return false;
4939
+ }
4940
+ this.logger.info(`Auto-attaching process signals on ${trigger}`);
4941
+ this.attachSignals();
4942
+ return true;
4943
+ }
4944
+ autoDetachSignalsIfIdle(trigger) {
4945
+ if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
4946
+ return;
4947
+ }
4948
+ this.logger.info(`Auto-detaching process signals after ${trigger}`);
4949
+ this.detachSignals();
4950
+ }
4951
+ monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
4952
+ this.logger.entity(name).warn(
4953
+ "Startup timed out without onStartupAborted, stopping component if startup completes later"
4954
+ );
4955
+ Promise.resolve(startPromise).then(async () => {
4956
+ const timeoutState = this.componentStates.get(name);
4957
+ if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
4958
+ return;
4959
+ }
4960
+ this.componentStates.set(name, "running");
4961
+ this.runningComponents.add(name);
4962
+ this.stalledComponents.delete(name);
4963
+ this.updateStartedFlag();
4964
+ const timestamps = this.componentTimestamps.get(name) ?? {
4965
+ startedAt: null,
4966
+ stoppedAt: null
4967
+ };
4968
+ timestamps.startedAt = Date.now();
4969
+ this.componentTimestamps.set(name, timestamps);
4970
+ this.logger.entity(name).warn(
4971
+ "Component completed startup after timeout, stopping automatically"
4972
+ );
4973
+ const stopResult = await this.stopComponentInternal(name);
4974
+ if (!stopResult.success) {
4975
+ this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
4976
+ params: {
4977
+ error: stopResult.error,
4978
+ code: stopResult.code
4979
+ }
4980
+ });
4981
+ return;
4982
+ }
4983
+ this.componentStates.set(name, timeoutState);
4984
+ }).catch(() => {
4985
+ });
4986
+ }
4849
4987
  /**
4850
4988
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
4851
4989
  */