lifecycleion 0.0.1 → 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.1
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;
@@ -2598,7 +2608,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2598
2608
  const err = error;
2599
2609
  const code = err instanceof DependencyCycleError ? "dependency_cycle" : "unknown_error";
2600
2610
  this.logger.error("Failed to resolve startup order", {
2601
- params: { error: err.message }
2611
+ params: { error: err }
2602
2612
  });
2603
2613
  return {
2604
2614
  success: false,
@@ -2688,10 +2698,22 @@ 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", {
2694
- params: { error: result.error?.message }
2716
+ params: { error: result.error }
2695
2717
  });
2696
2718
  this.lifecycleEvents.componentStartFailedOptional(
2697
2719
  name,
@@ -2707,7 +2729,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2707
2729
  });
2708
2730
  } else {
2709
2731
  this.logger.entity(name).error("Required component failed to start, rolling back", {
2710
- params: { error: result.error?.message }
2732
+ params: { error: result.error }
2711
2733
  });
2712
2734
  await this.rollbackStartup(startedComponents);
2713
2735
  return {
@@ -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
  }
@@ -3190,7 +3215,9 @@ var LifecycleManager = class extends EventEmitterProtected {
3190
3215
  } catch (error) {
3191
3216
  const durationMS = Date.now() - startTime;
3192
3217
  const err = error;
3193
- this.logger.entity(name).error("Health check failed", { params: { error: err.message } });
3218
+ this.logger.entity(name).error("Health check failed", {
3219
+ params: { error: err }
3220
+ });
3194
3221
  this.lifecycleEvents.componentHealthCheckFailed(name, err);
3195
3222
  return {
3196
3223
  name,
@@ -3337,7 +3364,9 @@ var LifecycleManager = class extends EventEmitterProtected {
3337
3364
  result = component.onMessage(payload, from);
3338
3365
  } catch (error) {
3339
3366
  const err = error instanceof Error ? error : new Error(String(error));
3340
- this.logger.entity(componentName).error("Message handler failed", { params: { error: err, from } });
3367
+ this.logger.entity(componentName).error("Message handler failed", {
3368
+ params: { error: err, from }
3369
+ });
3341
3370
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
3342
3371
  timedOut: false,
3343
3372
  code: "error",
@@ -3770,7 +3799,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3770
3799
  startupOrder2 = this.getStartupOrderInternal();
3771
3800
  } catch (error) {
3772
3801
  this.logger.warn("Failed to compute startup order in error handler", {
3773
- params: { error: error instanceof Error ? error.message : error }
3802
+ params: { error }
3774
3803
  });
3775
3804
  startupOrder2 = [];
3776
3805
  }
@@ -3839,6 +3868,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3839
3868
  stoppedAt: null
3840
3869
  });
3841
3870
  this.componentErrors.set(componentName, null);
3871
+ this.componentStartAttemptTokens.set(componentName, (0, import_ulid2.ulid)());
3842
3872
  const isManualPositionRespected = this.isManualPositionRespected({
3843
3873
  componentName,
3844
3874
  position,
@@ -3990,6 +4020,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3990
4020
  };
3991
4021
  }
3992
4022
  this.isShuttingDown = true;
4023
+ this.shutdownToken = (0, import_ulid2.ulid)();
3993
4024
  this.shutdownMethod = method;
3994
4025
  const isDuringStartup = this.isStarting;
3995
4026
  this.logger.info("Stopping all components", { params: { method } });
@@ -4005,7 +4036,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4005
4036
  this.logger.warn(
4006
4037
  "Could not resolve shutdown order, using registration order",
4007
4038
  {
4008
- params: { error: error.message }
4039
+ params: { error }
4009
4040
  }
4010
4041
  );
4011
4042
  shutdownOrder = this.components.map((c) => c.getName()).reverse();
@@ -4063,7 +4094,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4063
4094
  stoppedComponents.push(name);
4064
4095
  } else {
4065
4096
  this.logger.entity(name).error("Component failed to stop, continuing with others", {
4066
- params: { error: result2.error?.message }
4097
+ params: { error: result2.error }
4067
4098
  });
4068
4099
  const stallInfo = this.stalledComponents.get(name);
4069
4100
  if (stallInfo) {
@@ -4260,6 +4291,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4260
4291
  this.logger.entity(name).info("Starting component");
4261
4292
  this.lifecycleEvents.componentStarting(name);
4262
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;
4263
4298
  let timeoutHandle;
4264
4299
  try {
4265
4300
  const startPromise = component.start();
@@ -4274,6 +4309,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4274
4309
  params: { error }
4275
4310
  });
4276
4311
  }
4312
+ } else {
4313
+ this.monitorLateStartupCompletion(
4314
+ name,
4315
+ component,
4316
+ startPromise,
4317
+ startAttemptToken
4318
+ );
4277
4319
  }
4278
4320
  Promise.resolve(startPromise).catch(() => {
4279
4321
  });
@@ -4289,15 +4331,36 @@ var LifecycleManager = class extends EventEmitterProtected {
4289
4331
  } else {
4290
4332
  await startPromise;
4291
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
+ }
4292
4358
  this.componentStates.set(name, "running");
4293
4359
  this.runningComponents.add(name);
4294
4360
  this.stalledComponents.delete(name);
4295
4361
  this.updateStartedFlag();
4296
- if (this.attachSignalsOnStart && this.runningComponents.size === 1 && !this.processSignalManager) {
4297
- this.logger.info(
4298
- "Auto-attaching process signals on first component start"
4299
- );
4300
- this.attachSignals();
4362
+ if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4363
+ this.autoAttachSignals("first component start");
4301
4364
  }
4302
4365
  const timestamps = this.componentTimestamps.get(name) ?? {
4303
4366
  startedAt: null,
@@ -4319,7 +4382,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4319
4382
  if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4320
4383
  this.componentStates.set(name, "starting-timed-out");
4321
4384
  this.logger.entity(name).error("Component startup timed out", {
4322
- params: { error: err.message }
4385
+ params: { error: err }
4323
4386
  });
4324
4387
  this.lifecycleEvents.componentStartTimeout(name, err, {
4325
4388
  timeoutMS,
@@ -4328,7 +4391,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4328
4391
  } else {
4329
4392
  this.componentStates.set(name, "registered");
4330
4393
  this.logger.entity(name).error("Component failed to start", {
4331
- params: { error: err.message }
4394
+ params: { error: err }
4332
4395
  });
4333
4396
  this.lifecycleEvents.componentStartFailed(name, err, {
4334
4397
  reason: err.message
@@ -4346,6 +4409,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4346
4409
  if (timeoutHandle) {
4347
4410
  clearTimeout(timeoutHandle);
4348
4411
  }
4412
+ if (didAutoAttachSignalsForComponentStartup) {
4413
+ this.autoDetachSignalsIfIdle("failed component startup");
4414
+ }
4349
4415
  }
4350
4416
  }
4351
4417
  /**
@@ -4457,7 +4523,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4457
4523
  this.lifecycleEvents.componentShutdownWarningCompleted(name);
4458
4524
  }).catch((error) => {
4459
4525
  this.logger.entity(name).warn("Shutdown warning phase failed", {
4460
- params: { error: error.message }
4526
+ params: { error }
4461
4527
  });
4462
4528
  });
4463
4529
  }
@@ -4480,7 +4546,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4480
4546
  }).catch((error) => {
4481
4547
  statuses.set(name, "rejected");
4482
4548
  this.logger.entity(name).warn("Shutdown warning phase failed", {
4483
- params: { error: error.message }
4549
+ params: { error }
4484
4550
  });
4485
4551
  })
4486
4552
  );
@@ -4603,7 +4669,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4603
4669
  };
4604
4670
  } else {
4605
4671
  this.logger.entity(name).warn("Graceful shutdown threw error", {
4606
- params: { error: err.message }
4672
+ params: { error: err }
4607
4673
  });
4608
4674
  return {
4609
4675
  success: false,
@@ -4747,7 +4813,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4747
4813
  this.lifecycleEvents.componentShutdownForceTimeout(name, timeoutMS);
4748
4814
  } else {
4749
4815
  this.logger.entity(name).error("Force shutdown failed - stalled", {
4750
- params: { error: err.message }
4816
+ params: { error: err }
4751
4817
  });
4752
4818
  }
4753
4819
  this.lifecycleEvents.componentStalled(name, stallInfo, {
@@ -4836,12 +4902,63 @@ var LifecycleManager = class extends EventEmitterProtected {
4836
4902
  const result = await this.stopComponentInternal(name);
4837
4903
  if (!result.success) {
4838
4904
  this.logger.entity(name).warn("Failed to stop component during rollback, continuing", {
4839
- params: { error: result.error?.message }
4905
+ params: { error: result.error }
4840
4906
  });
4841
4907
  }
4842
4908
  }
4843
4909
  this.logger.info("Rollback completed");
4844
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
+ }
4845
4962
  /**
4846
4963
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
4847
4964
  */
@@ -4860,7 +4977,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4860
4977
  startupOrder = this.getStartupOrderInternal();
4861
4978
  } catch (error) {
4862
4979
  this.logger.warn("Failed to compute startup order in error handler", {
4863
- params: { error: error instanceof Error ? error.message : error }
4980
+ params: { error }
4864
4981
  });
4865
4982
  startupOrder = [];
4866
4983
  }
@@ -4883,7 +5000,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4883
5000
  startupOrder = this.getStartupOrderInternal();
4884
5001
  } catch (error) {
4885
5002
  this.logger.warn("Failed to compute startup order in error handler", {
4886
- params: { error: error instanceof Error ? error.message : error }
5003
+ params: { error }
4887
5004
  });
4888
5005
  startupOrder = [];
4889
5006
  }