lifecycleion 0.0.2 → 0.0.3

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.3
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,13 @@ 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)();
1982
1989
  shutdownMethod = null;
1983
1990
  lastShutdownResult = null;
1984
1991
  // Signal management
@@ -2004,6 +2011,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2004
2011
  haltOnStall: true,
2005
2012
  ...options.shutdownOptions
2006
2013
  };
2014
+ this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
2007
2015
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
2008
2016
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
2009
2017
  this.onReloadRequested = options.onReloadRequested;
@@ -2167,6 +2175,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2167
2175
  this.componentStates.delete(name);
2168
2176
  this.componentTimestamps.delete(name);
2169
2177
  this.componentErrors.delete(name);
2178
+ this.componentStartAttemptTokens.delete(name);
2170
2179
  this.stalledComponents.delete(name);
2171
2180
  this.runningComponents.delete(name);
2172
2181
  this.updateStartedFlag();
@@ -2575,6 +2584,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2575
2584
  this.isStarting = true;
2576
2585
  this.shutdownMethod = null;
2577
2586
  this.lastShutdownResult = null;
2587
+ const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
2578
2588
  this.logger.info("Starting all components");
2579
2589
  const effectiveTimeout = options?.timeoutMS ?? this.startupTimeoutMS;
2580
2590
  let hasTimedOut = false;
@@ -2688,6 +2698,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2688
2698
  startedComponents.push(name);
2689
2699
  } else if (result.code === "component_already_running") {
2690
2700
  startedComponents.push(name);
2701
+ } else if (result.code === "shutdown_in_progress") {
2702
+ await this.rollbackStartup(startedComponents);
2703
+ return {
2704
+ success: false,
2705
+ startedComponents: [],
2706
+ failedOptionalComponents,
2707
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2708
+ reason: result.reason || "Shutdown triggered during startup",
2709
+ code: "shutdown_in_progress",
2710
+ error: result.error,
2711
+ durationMS: Date.now() - startTime
2712
+ };
2691
2713
  } else {
2692
2714
  if (component.isOptional()) {
2693
2715
  this.logger.entity(name).warn("Optional component failed to start, continuing", {
@@ -2776,6 +2798,9 @@ var LifecycleManager = class extends EventEmitterProtected {
2776
2798
  if (timeoutHandle) {
2777
2799
  clearTimeout(timeoutHandle);
2778
2800
  }
2801
+ if (didAutoAttachSignalsForBulkStartup) {
2802
+ this.autoDetachSignalsIfIdle("failed bulk startup");
2803
+ }
2779
2804
  this.isStarting = false;
2780
2805
  }
2781
2806
  }
@@ -3843,6 +3868,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3843
3868
  stoppedAt: null
3844
3869
  });
3845
3870
  this.componentErrors.set(componentName, null);
3871
+ this.componentStartAttemptTokens.set(componentName, (0, import_ulid2.ulid)());
3846
3872
  const isManualPositionRespected = this.isManualPositionRespected({
3847
3873
  componentName,
3848
3874
  position,
@@ -3994,6 +4020,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3994
4020
  };
3995
4021
  }
3996
4022
  this.isShuttingDown = true;
4023
+ this.shutdownToken = (0, import_ulid2.ulid)();
3997
4024
  this.shutdownMethod = method;
3998
4025
  const isDuringStartup = this.isStarting;
3999
4026
  this.logger.info("Stopping all components", { params: { method } });
@@ -4264,6 +4291,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4264
4291
  this.logger.entity(name).info("Starting component");
4265
4292
  this.lifecycleEvents.componentStarting(name);
4266
4293
  const timeoutMS = component.startupTimeoutMS;
4294
+ const startAttemptToken = (0, import_ulid2.ulid)();
4295
+ this.componentStartAttemptTokens.set(name, startAttemptToken);
4296
+ const shutdownTokenAtStart = this.shutdownToken;
4297
+ const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4267
4298
  let timeoutHandle;
4268
4299
  try {
4269
4300
  const startPromise = component.start();
@@ -4278,6 +4309,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4278
4309
  params: { error }
4279
4310
  });
4280
4311
  }
4312
+ } else {
4313
+ this.monitorLateStartupCompletion(
4314
+ name,
4315
+ component,
4316
+ startPromise,
4317
+ startAttemptToken
4318
+ );
4281
4319
  }
4282
4320
  Promise.resolve(startPromise).catch(() => {
4283
4321
  });
@@ -4293,15 +4331,36 @@ var LifecycleManager = class extends EventEmitterProtected {
4293
4331
  } else {
4294
4332
  await startPromise;
4295
4333
  }
4334
+ if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4335
+ this.componentStates.set(name, "running");
4336
+ this.runningComponents.add(name);
4337
+ this.stalledComponents.delete(name);
4338
+ this.updateStartedFlag();
4339
+ const timestamps2 = this.componentTimestamps.get(name) ?? {
4340
+ startedAt: null,
4341
+ stoppedAt: null
4342
+ };
4343
+ timestamps2.startedAt = Date.now();
4344
+ this.componentTimestamps.set(name, timestamps2);
4345
+ this.logger.entity(name).warn(
4346
+ "Component finished starting after shutdown began, stopping immediately"
4347
+ );
4348
+ const stopResult = await this.stopComponentInternal(name);
4349
+ return {
4350
+ success: false,
4351
+ componentName: name,
4352
+ reason: "Shutdown triggered during component startup",
4353
+ code: "shutdown_in_progress",
4354
+ error: stopResult.error,
4355
+ status: this.getComponentStatus(name)
4356
+ };
4357
+ }
4296
4358
  this.componentStates.set(name, "running");
4297
4359
  this.runningComponents.add(name);
4298
4360
  this.stalledComponents.delete(name);
4299
4361
  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();
4362
+ if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4363
+ this.autoAttachSignals("first component start");
4305
4364
  }
4306
4365
  const timestamps = this.componentTimestamps.get(name) ?? {
4307
4366
  startedAt: null,
@@ -4350,6 +4409,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4350
4409
  if (timeoutHandle) {
4351
4410
  clearTimeout(timeoutHandle);
4352
4411
  }
4412
+ if (didAutoAttachSignalsForComponentStartup) {
4413
+ this.autoDetachSignalsIfIdle("failed component startup");
4414
+ }
4353
4415
  }
4354
4416
  }
4355
4417
  /**
@@ -4846,6 +4908,57 @@ var LifecycleManager = class extends EventEmitterProtected {
4846
4908
  }
4847
4909
  this.logger.info("Rollback completed");
4848
4910
  }
4911
+ autoAttachSignals(trigger) {
4912
+ if (this.processSignalManager?.getStatus().isAttached) {
4913
+ return false;
4914
+ }
4915
+ this.logger.info(`Auto-attaching process signals on ${trigger}`);
4916
+ this.attachSignals();
4917
+ return true;
4918
+ }
4919
+ autoDetachSignalsIfIdle(trigger) {
4920
+ if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
4921
+ return;
4922
+ }
4923
+ this.logger.info(`Auto-detaching process signals after ${trigger}`);
4924
+ this.detachSignals();
4925
+ }
4926
+ monitorLateStartupCompletion(name, component, startPromise, startAttemptToken) {
4927
+ this.logger.entity(name).warn(
4928
+ "Startup timed out without onStartupAborted, stopping component if startup completes later"
4929
+ );
4930
+ Promise.resolve(startPromise).then(async () => {
4931
+ const timeoutState = this.componentStates.get(name);
4932
+ if (this.getComponent(name) !== component || this.componentStartAttemptTokens.get(name) !== startAttemptToken || this.isComponentRunning(name) || timeoutState !== "starting-timed-out" && timeoutState !== "failed") {
4933
+ return;
4934
+ }
4935
+ this.componentStates.set(name, "running");
4936
+ this.runningComponents.add(name);
4937
+ this.stalledComponents.delete(name);
4938
+ this.updateStartedFlag();
4939
+ const timestamps = this.componentTimestamps.get(name) ?? {
4940
+ startedAt: null,
4941
+ stoppedAt: null
4942
+ };
4943
+ timestamps.startedAt = Date.now();
4944
+ this.componentTimestamps.set(name, timestamps);
4945
+ this.logger.entity(name).warn(
4946
+ "Component completed startup after timeout, stopping automatically"
4947
+ );
4948
+ const stopResult = await this.stopComponentInternal(name);
4949
+ if (!stopResult.success) {
4950
+ this.logger.entity(name).warn("Automatic stop after startup timeout failed", {
4951
+ params: {
4952
+ error: stopResult.error,
4953
+ code: stopResult.code
4954
+ }
4955
+ });
4956
+ return;
4957
+ }
4958
+ this.componentStates.set(name, timeoutState);
4959
+ }).catch(() => {
4960
+ });
4961
+ }
4849
4962
  /**
4850
4963
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
4851
4964
  */