lifecycleion 0.0.13 → 0.0.15

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.
@@ -1263,6 +1263,16 @@ var LifecycleManagerEvents = class {
1263
1263
  code: info?.code
1264
1264
  });
1265
1265
  }
1266
+ componentStalledResolved(name, stallInfo, stalledDurationMS) {
1267
+ this.emit("component:stalled-resolved", {
1268
+ name,
1269
+ stallInfo,
1270
+ stalledDurationMS
1271
+ });
1272
+ }
1273
+ componentUnexpectedStop(name, error) {
1274
+ this.emit("component:unexpected-stop", { name, error });
1275
+ }
1266
1276
  componentShutdownForceCompleted(name) {
1267
1277
  this.emit("component:shutdown-force-completed", { name });
1268
1278
  }
@@ -1449,6 +1459,26 @@ var lifecycleManagerErrCodes = {
1449
1459
  StopTimeout: "StopTimeout"
1450
1460
  };
1451
1461
 
1462
+ // src/lib/lifecycle-manager/constants.ts
1463
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
1464
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
1465
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
1466
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
1467
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
1468
+ var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
1469
+ var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
1470
+ var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
1471
+ var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
1472
+ var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
1473
+ var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
1474
+ var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
1475
+ var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
1476
+ var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
1477
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
1478
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
1479
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
1480
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE_EXTERNAL = "Component instance is already registered with another lifecycle manager.";
1481
+
1452
1482
  // src/lib/process-signal-manager.ts
1453
1483
  var import_ulid = require("ulid");
1454
1484
  var import_readline = __toESM(require("readline"), 1);
@@ -1993,8 +2023,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1993
2023
  componentTimestamps = /* @__PURE__ */ new Map();
1994
2024
  componentErrors = /* @__PURE__ */ new Map();
1995
2025
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
2026
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
2027
+ // component can be unregistered and replaced by a same-name instance before
2028
+ // the old floating stop promise settles.
2029
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
2030
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
2031
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1996
2032
  // State flags
1997
2033
  isStarting = false;
2034
+ autoAttachedSignalsDuringStartup = false;
1998
2035
  isStarted = false;
1999
2036
  isShuttingDown = false;
2000
2037
  // Unique token used to detect shutdowns that happened during async start().
@@ -2143,7 +2180,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2143
2180
  */
2144
2181
  async unregisterComponent(name, options) {
2145
2182
  if (this.isStarting || this.isShuttingDown) {
2146
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2183
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2147
2184
  params: {
2148
2185
  isStarting: this.isStarting,
2149
2186
  isShuttingDown: this.isShuttingDown
@@ -2152,7 +2189,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2152
2189
  return {
2153
2190
  success: false,
2154
2191
  componentName: name,
2155
- reason: "Cannot unregister during bulk operation",
2192
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2156
2193
  code: "bulk_operation_in_progress",
2157
2194
  wasStopped: false,
2158
2195
  wasRegistered: this.hasComponent(name)
@@ -2160,11 +2197,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2160
2197
  }
2161
2198
  const component = this.getComponent(name);
2162
2199
  if (!component) {
2163
- this.logger.entity(name).warn("Component not found");
2200
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2164
2201
  return {
2165
2202
  success: false,
2166
2203
  componentName: name,
2167
- reason: "Component not found",
2204
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2168
2205
  code: "component_not_found",
2169
2206
  wasStopped: false,
2170
2207
  wasRegistered: false
@@ -2177,7 +2214,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2177
2214
  return {
2178
2215
  success: false,
2179
2216
  componentName: name,
2180
- reason: "Component is stalled",
2217
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2181
2218
  code: "stop_failed",
2182
2219
  stopFailureReason: "stalled",
2183
2220
  wasStopped: false,
@@ -2229,10 +2266,14 @@ var LifecycleManager = class extends EventEmitterProtected {
2229
2266
  wasStopped = true;
2230
2267
  }
2231
2268
  this.components = this.components.filter((c) => c.getName() !== name);
2269
+ component._clearUnexpectedStopHandler();
2270
+ component._markUnregistered();
2232
2271
  this.componentStates.delete(name);
2233
2272
  this.componentTimestamps.delete(name);
2234
2273
  this.componentErrors.delete(name);
2235
2274
  this.componentStartAttemptTokens.delete(name);
2275
+ this.componentStopAttemptTokens.delete(name);
2276
+ this.pendingForceStopWaiters.delete(name);
2236
2277
  this.stalledComponents.delete(name);
2237
2278
  this.runningComponents.delete(name);
2238
2279
  this.updateStartedFlag();
@@ -2579,7 +2620,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2579
2620
  startedComponents: [],
2580
2621
  failedOptionalComponents: [],
2581
2622
  skippedDueToDependency: [],
2582
- reason: "Shutdown in progress",
2623
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2583
2624
  code: "shutdown_in_progress",
2584
2625
  durationMS: Date.now() - startTime
2585
2626
  };
@@ -2639,6 +2680,8 @@ var LifecycleManager = class extends EventEmitterProtected {
2639
2680
  };
2640
2681
  }
2641
2682
  this.isStarting = true;
2683
+ this.autoAttachedSignalsDuringStartup = false;
2684
+ this.unexpectedStopsDuringStartup.clear();
2642
2685
  this.resetRepeatedShutdownRequestState();
2643
2686
  this.shutdownMethod = null;
2644
2687
  this.lastShutdownResult = null;
@@ -2771,13 +2814,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2771
2814
  error: result.error,
2772
2815
  durationMS: Date.now() - startTime
2773
2816
  };
2817
+ } else if (result.code === "component_unexpected_stop") {
2818
+ this.unexpectedStopsDuringStartup.delete(name);
2819
+ const error = result.error || new Error(
2820
+ result.reason || `Component "${name}" stopped unexpectedly`
2821
+ );
2822
+ if (component.isOptional()) {
2823
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2824
+ failedOptionalComponents.push({ name, error });
2825
+ }
2826
+ this.logger.entity(name).warn(
2827
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2828
+ {
2829
+ params: { error }
2830
+ }
2831
+ );
2832
+ } else {
2833
+ this.logger.entity(name).error(
2834
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2835
+ {
2836
+ params: { error }
2837
+ }
2838
+ );
2839
+ await this.rollbackStartup(startedComponents);
2840
+ return {
2841
+ success: false,
2842
+ startedComponents: [],
2843
+ failedOptionalComponents,
2844
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2845
+ reason: error.message,
2846
+ code: "component_unexpected_stop",
2847
+ error,
2848
+ durationMS: Date.now() - startTime
2849
+ };
2850
+ }
2774
2851
  } else {
2775
2852
  if (component.isOptional()) {
2776
2853
  this.logger.entity(name).warn(
2777
2854
  "Optional component failed to start, continuing: {{error.message}}",
2778
2855
  {
2779
2856
  params: {
2780
- error: result.error || new Error(result.reason || "Unknown error")
2857
+ error: result.error || new Error(
2858
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2859
+ )
2781
2860
  }
2782
2861
  }
2783
2862
  );
@@ -2791,14 +2870,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2791
2870
  }
2792
2871
  failedOptionalComponents.push({
2793
2872
  name,
2794
- error: result.error || new Error(result.reason || "Unknown error")
2873
+ error: result.error || new Error(
2874
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2875
+ )
2795
2876
  });
2796
2877
  } else {
2797
2878
  this.logger.entity(name).error(
2798
2879
  "Required component failed to start, rolling back: {{error.message}}",
2799
2880
  {
2800
2881
  params: {
2801
- error: result.error || new Error(result.reason || "Unknown error")
2882
+ error: result.error || new Error(
2883
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2884
+ )
2802
2885
  }
2803
2886
  }
2804
2887
  );
@@ -2815,6 +2898,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2815
2898
  };
2816
2899
  }
2817
2900
  }
2901
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2902
+ startedComponents,
2903
+ failedOptionalComponents
2904
+ );
2905
+ startedComponents.splice(0, startedComponents.length);
2906
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2907
+ if (unexpectedStopResult2.requiredFailure) {
2908
+ await this.rollbackStartup(startedComponents);
2909
+ return {
2910
+ success: false,
2911
+ startedComponents: [],
2912
+ failedOptionalComponents,
2913
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2914
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2915
+ code: "component_unexpected_stop",
2916
+ error: unexpectedStopResult2.requiredFailure.error,
2917
+ durationMS: Date.now() - startTime
2918
+ };
2919
+ }
2818
2920
  }
2819
2921
  if (hasTimedOut) {
2820
2922
  const durationMS2 = Date.now() - startTime;
@@ -2838,6 +2940,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2838
2940
  code: "startup_timeout"
2839
2941
  };
2840
2942
  }
2943
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2944
+ startedComponents,
2945
+ failedOptionalComponents
2946
+ );
2947
+ startedComponents.splice(0, startedComponents.length);
2948
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2949
+ if (unexpectedStopResult.requiredFailure) {
2950
+ await this.rollbackStartup(startedComponents);
2951
+ return {
2952
+ success: false,
2953
+ startedComponents: [],
2954
+ failedOptionalComponents,
2955
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2956
+ reason: unexpectedStopResult.requiredFailure.error.message,
2957
+ code: "component_unexpected_stop",
2958
+ error: unexpectedStopResult.requiredFailure.error,
2959
+ durationMS: Date.now() - startTime
2960
+ };
2961
+ }
2841
2962
  this.updateStartedFlag();
2842
2963
  const skippedComponentsArray = [
2843
2964
  ...Array.from(skippedDueToDependency),
@@ -2869,10 +2990,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2869
2990
  if (timeoutHandle) {
2870
2991
  clearTimeout(timeoutHandle);
2871
2992
  }
2872
- if (didAutoAttachSignalsForBulkStartup) {
2993
+ this.isStarting = false;
2994
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2873
2995
  this.autoDetachSignalsIfIdle("failed bulk startup");
2874
2996
  }
2875
- this.isStarting = false;
2997
+ this.autoAttachedSignalsDuringStartup = false;
2998
+ this.unexpectedStopsDuringStartup.clear();
2876
2999
  }
2877
3000
  }
2878
3001
  /**
@@ -2936,7 +3059,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2936
3059
  return {
2937
3060
  success: false,
2938
3061
  componentName: name,
2939
- reason: "Bulk startup in progress",
3062
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2940
3063
  code: "startup_in_progress"
2941
3064
  };
2942
3065
  }
@@ -2947,7 +3070,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2947
3070
  return {
2948
3071
  success: false,
2949
3072
  componentName: name,
2950
- reason: "Shutdown in progress",
3073
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2951
3074
  code: "shutdown_in_progress"
2952
3075
  };
2953
3076
  }
@@ -2981,7 +3104,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2981
3104
  return {
2982
3105
  success: false,
2983
3106
  componentName: name,
2984
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3107
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2985
3108
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2986
3109
  };
2987
3110
  }
@@ -3163,7 +3286,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3163
3286
  if (this.isShuttingDown) {
3164
3287
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3165
3288
  this.logger.debug(
3166
- "Logger exit called during shutdown, waiting...",
3289
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3167
3290
  {
3168
3291
  params: { exitCode }
3169
3292
  }
@@ -3172,7 +3295,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3172
3295
  this.pendingLoggerExitResolve = resolve;
3173
3296
  });
3174
3297
  }
3175
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3298
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3176
3299
  params: { exitCode }
3177
3300
  });
3178
3301
  return { action: "wait" };
@@ -3262,7 +3385,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3262
3385
  return {
3263
3386
  name,
3264
3387
  healthy: false,
3265
- message: "Component not found",
3388
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3266
3389
  checkedAt: startTime,
3267
3390
  durationMS: 0,
3268
3391
  error: null,
@@ -3275,7 +3398,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3275
3398
  return {
3276
3399
  name,
3277
3400
  healthy: false,
3278
- message: isStalled ? "Component is stalled" : "Component not running",
3401
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3279
3402
  checkedAt: startTime,
3280
3403
  durationMS: Date.now() - startTime,
3281
3404
  error: null,
@@ -3491,7 +3614,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3491
3614
  result = component.onMessage(payload, from);
3492
3615
  } catch (error) {
3493
3616
  const err = error instanceof Error ? error : new Error(String(error));
3494
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3617
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3495
3618
  params: { error: err, from }
3496
3619
  });
3497
3620
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3551,7 +3674,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3551
3674
  };
3552
3675
  } catch (error) {
3553
3676
  const err = error instanceof Error ? error : new Error(String(error));
3554
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3677
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3555
3678
  params: { error: err, from, timeoutMS }
3556
3679
  });
3557
3680
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3824,7 +3947,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3824
3947
  this.lifecycleEvents.componentRegistrationRejected({
3825
3948
  name: componentName,
3826
3949
  reason: "shutdown_in_progress",
3827
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3950
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3828
3951
  registrationIndexBefore,
3829
3952
  registrationIndexAfter: registrationIndexBefore,
3830
3953
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3836,7 +3959,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3836
3959
  targetComponentName,
3837
3960
  registrationIndexBefore,
3838
3961
  code: "shutdown_in_progress",
3839
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3962
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3840
3963
  targetFound: void 0
3841
3964
  });
3842
3965
  }
@@ -3847,7 +3970,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3847
3970
  this.lifecycleEvents.componentRegistrationRejected({
3848
3971
  name: componentName,
3849
3972
  reason: "startup_in_progress",
3850
- message: "Cannot register component during startup when it is a required dependency for other components.",
3973
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3851
3974
  registrationIndexBefore,
3852
3975
  registrationIndexAfter: registrationIndexBefore,
3853
3976
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3859,16 +3982,20 @@ var LifecycleManager = class extends EventEmitterProtected {
3859
3982
  targetComponentName,
3860
3983
  registrationIndexBefore,
3861
3984
  code: "startup_in_progress",
3862
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3985
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3863
3986
  targetFound: void 0
3864
3987
  });
3865
3988
  }
3866
- if (this.hasComponentInstance(component)) {
3867
- this.logger.entity(componentName).warn("Component instance already registered");
3989
+ if (component._isRegisteredWithManager()) {
3990
+ const isRegisteredHere = this.hasComponentInstance(component);
3991
+ const message = isRegisteredHere ? LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE : LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE_EXTERNAL;
3992
+ this.logger.entity(componentName).warn(
3993
+ isRegisteredHere ? "Component instance already registered" : "Component instance already registered with another lifecycle manager"
3994
+ );
3868
3995
  this.lifecycleEvents.componentRegistrationRejected({
3869
3996
  name: componentName,
3870
3997
  reason: "duplicate_instance",
3871
- message: "Component instance is already registered.",
3998
+ message,
3872
3999
  registrationIndexBefore,
3873
4000
  registrationIndexAfter: registrationIndexBefore,
3874
4001
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3880,7 +4007,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3880
4007
  targetComponentName,
3881
4008
  registrationIndexBefore,
3882
4009
  code: "duplicate_instance",
3883
- reason: "Component instance is already registered.",
4010
+ reason: message,
3884
4011
  targetFound: void 0
3885
4012
  });
3886
4013
  }
@@ -3993,6 +4120,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3993
4120
  getValueInternal: (compName, key, from) => this.getValueInternal(compName, key, from)
3994
4121
  };
3995
4122
  component.lifecycle = new ComponentLifecycle(this, componentName, internalCallbacks);
4123
+ component._markRegistered();
3996
4124
  this.componentStates.set(componentName, "registered");
3997
4125
  this.componentTimestamps.set(componentName, {
3998
4126
  startedAt: null,
@@ -4194,8 +4322,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4194
4322
  const runningComponentsToStop = shutdownOrder.filter(
4195
4323
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4196
4324
  );
4197
- const stoppedComponents = [];
4198
- const stalledComponents = [];
4325
+ const stoppedComponents = /* @__PURE__ */ new Set();
4199
4326
  let hasTimedOut = false;
4200
4327
  let timeoutHandle;
4201
4328
  try {
@@ -4226,34 +4353,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4226
4353
  this.logger.entity(name).info("Stopping component");
4227
4354
  const isRunning = this.isComponentRunning(name);
4228
4355
  const isStalled = stalledComponentNames.has(name);
4356
+ const currentState = this.componentStates.get(name);
4357
+ if (currentState === "stopped") {
4358
+ stoppedComponents.add(name);
4359
+ continue;
4360
+ }
4229
4361
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4230
4362
  success: false,
4231
4363
  componentName: name,
4232
- reason: "Component is stalled",
4364
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4233
4365
  code: "component_stalled",
4234
4366
  status: this.getComponentStatus(name)
4235
4367
  } : {
4236
4368
  success: false,
4237
4369
  componentName: name,
4238
- reason: "Component not running",
4370
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4239
4371
  code: "component_not_running",
4240
4372
  status: this.getComponentStatus(name)
4241
4373
  };
4242
4374
  if (result2.success) {
4243
- stoppedComponents.push(name);
4375
+ stoppedComponents.add(name);
4244
4376
  } else {
4245
4377
  this.logger.entity(name).error(
4246
4378
  "Component failed to stop, continuing with others: {{error.message}}",
4247
4379
  {
4248
4380
  params: {
4249
- error: result2.error || new Error(result2.reason || "Unknown error")
4381
+ error: result2.error || new Error(
4382
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4383
+ )
4250
4384
  }
4251
4385
  }
4252
4386
  );
4253
- const stallInfo = this.stalledComponents.get(name);
4254
- if (stallInfo) {
4255
- stalledComponents.push(stallInfo);
4256
- }
4257
4387
  if (shouldHaltOnStall) {
4258
4388
  this.logger.warn(
4259
4389
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4269,21 +4399,32 @@ var LifecycleManager = class extends EventEmitterProtected {
4269
4399
  } else {
4270
4400
  await shutdownOperation();
4271
4401
  }
4402
+ const finalStalledNames = /* @__PURE__ */ new Set();
4403
+ for (const name of runningComponentsToStop) {
4404
+ if (this.stalledComponents.has(name)) {
4405
+ finalStalledNames.add(name);
4406
+ }
4407
+ }
4272
4408
  if (!shouldRetryStalled) {
4273
4409
  for (const name of stalledComponentNames) {
4274
- const stallInfo = this.stalledComponents.get(name);
4275
- if (stallInfo && !stalledComponents.some((component) => component.name === name)) {
4276
- stalledComponents.push(stallInfo);
4410
+ if (this.stalledComponents.has(name)) {
4411
+ finalStalledNames.add(name);
4277
4412
  }
4278
4413
  }
4279
4414
  }
4415
+ for (const name of runningComponentsToStop) {
4416
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4417
+ stoppedComponents.add(name);
4418
+ }
4419
+ }
4420
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4280
4421
  const durationMS = Date.now() - startTime;
4281
4422
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4282
4423
  this.logger[isSuccess ? "success" : "warn"](
4283
4424
  isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4284
4425
  {
4285
4426
  params: {
4286
- stopped: stoppedComponents.length,
4427
+ stopped: stoppedComponents.size,
4287
4428
  stalled: stalledComponents.length,
4288
4429
  durationMS
4289
4430
  }
@@ -4291,7 +4432,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4291
4432
  );
4292
4433
  const result = {
4293
4434
  success: isSuccess,
4294
- stoppedComponents,
4435
+ stoppedComponents: Array.from(stoppedComponents),
4295
4436
  stalledComponents,
4296
4437
  durationMS,
4297
4438
  timedOut: hasTimedOut || void 0,
@@ -4342,7 +4483,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4342
4483
  return {
4343
4484
  success: false,
4344
4485
  componentName: name,
4345
- reason: "Component not found",
4486
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4346
4487
  code: "component_not_found"
4347
4488
  };
4348
4489
  }
@@ -4353,12 +4494,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4353
4494
  return {
4354
4495
  success: false,
4355
4496
  componentName: name,
4356
- reason: "Component not running",
4497
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4357
4498
  code: "component_not_running",
4358
4499
  status: this.getComponentStatus(name)
4359
4500
  };
4360
4501
  }
4361
4502
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4503
+ if (component.onShutdownForce) {
4504
+ this.issueStopAttemptToken(name);
4505
+ }
4362
4506
  return this.shutdownComponentForce(name, component, {
4363
4507
  gracefulPhaseRan: false,
4364
4508
  gracefulTimedOut: false,
@@ -4378,7 +4522,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4378
4522
  return {
4379
4523
  success: false,
4380
4524
  componentName: name,
4381
- reason: "Shutdown in progress",
4525
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4382
4526
  code: "shutdown_in_progress"
4383
4527
  };
4384
4528
  }
@@ -4390,7 +4534,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4390
4534
  return {
4391
4535
  success: false,
4392
4536
  componentName: name,
4393
- reason: "Bulk startup in progress",
4537
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4394
4538
  code: "startup_in_progress"
4395
4539
  };
4396
4540
  }
@@ -4400,7 +4544,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4400
4544
  return {
4401
4545
  success: false,
4402
4546
  componentName: name,
4403
- reason: "Component not found",
4547
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4404
4548
  code: "component_not_found"
4405
4549
  };
4406
4550
  }
@@ -4409,7 +4553,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4409
4553
  return {
4410
4554
  success: false,
4411
4555
  componentName: name,
4412
- reason: "Component is stalled",
4556
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4413
4557
  code: "component_stalled",
4414
4558
  status: this.getComponentStatus(name)
4415
4559
  };
@@ -4473,6 +4617,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4473
4617
  const timeoutMS = component.startupTimeoutMS;
4474
4618
  const startAttemptToken = (0, import_ulid2.ulid)();
4475
4619
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4620
+ component._setUnexpectedStopHandler(
4621
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4622
+ );
4476
4623
  const shutdownTokenAtStart = this.shutdownToken;
4477
4624
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4478
4625
  let timeoutHandle;
@@ -4515,6 +4662,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4515
4662
  } else {
4516
4663
  await startPromise;
4517
4664
  }
4665
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4666
+ component._clearUnexpectedStopHandler();
4667
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4668
+ return {
4669
+ success: false,
4670
+ componentName: name,
4671
+ reason: error.message,
4672
+ code: "component_unexpected_stop",
4673
+ error,
4674
+ status: this.getComponentStatus(name)
4675
+ };
4676
+ }
4518
4677
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4519
4678
  this.componentStates.set(name, "running");
4520
4679
  this.runningComponents.add(name);
@@ -4542,6 +4701,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4542
4701
  this.componentStates.set(name, "running");
4543
4702
  this.runningComponents.add(name);
4544
4703
  this.stalledComponents.delete(name);
4704
+ if (shouldForceStalled) {
4705
+ this.issueStopAttemptToken(name);
4706
+ }
4545
4707
  this.updateStartedFlag();
4546
4708
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4547
4709
  this.autoAttachSignals("first component start");
@@ -4561,9 +4723,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4561
4723
  status: this.getComponentStatus(name)
4562
4724
  };
4563
4725
  } catch (error) {
4726
+ component._clearUnexpectedStopHandler();
4564
4727
  const err = error instanceof Error ? error : new Error(String(error));
4728
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4729
+ const unexpectedStopError = this.componentErrors.get(name);
4730
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4731
+ return {
4732
+ success: false,
4733
+ componentName: name,
4734
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4735
+ code: "component_unexpected_stop",
4736
+ error: unexpectedStopError || new Error(
4737
+ `Component "${name}" stopped unexpectedly during startup`
4738
+ ),
4739
+ status: this.getComponentStatus(name)
4740
+ };
4741
+ }
4565
4742
  this.componentErrors.set(name, err);
4566
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4743
+ if (isStartupTimeout) {
4567
4744
  this.componentStates.set(name, "starting-timed-out");
4568
4745
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4569
4746
  params: { error: err }
@@ -4608,7 +4785,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4608
4785
  return {
4609
4786
  success: false,
4610
4787
  componentName: name,
4611
- reason: "Component not found",
4788
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4612
4789
  code: "component_not_found"
4613
4790
  };
4614
4791
  }
@@ -4616,7 +4793,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4616
4793
  return {
4617
4794
  success: false,
4618
4795
  componentName: name,
4619
- reason: "Component is stalled",
4796
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4620
4797
  code: "component_stalled",
4621
4798
  status: this.getComponentStatus(name)
4622
4799
  };
@@ -4625,7 +4802,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4625
4802
  return {
4626
4803
  success: false,
4627
4804
  componentName: name,
4628
- reason: "Component not running",
4805
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4629
4806
  code: "component_not_running",
4630
4807
  status: this.getComponentStatus(name)
4631
4808
  };
@@ -4641,6 +4818,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4641
4818
  };
4642
4819
  }
4643
4820
  if (options?.forceImmediate) {
4821
+ if (component.onShutdownForce) {
4822
+ this.issueStopAttemptToken(name);
4823
+ }
4824
+ component._clearUnexpectedStopHandler();
4644
4825
  return this.shutdownComponentForce(name, component, {
4645
4826
  gracefulPhaseRan: false,
4646
4827
  gracefulTimedOut: false,
@@ -4777,9 +4958,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4777
4958
  * Calls stop() with timeout
4778
4959
  */
4779
4960
  async shutdownComponentGraceful(name, component, options) {
4961
+ component._clearUnexpectedStopHandler();
4780
4962
  this.componentStates.set(name, "stopping");
4781
4963
  this.logger.entity(name).info("Graceful shutdown started");
4782
4964
  this.lifecycleEvents.componentStopping(name);
4965
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4783
4966
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4784
4967
  let timeoutHandle;
4785
4968
  try {
@@ -4800,7 +4983,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4800
4983
  );
4801
4984
  }
4802
4985
  }
4803
- Promise.resolve(stopPromise).catch(() => {
4986
+ Promise.resolve(stopPromise).then(
4987
+ () => this.handleLateStopResolution(
4988
+ name,
4989
+ stopAttemptToken,
4990
+ "graceful"
4991
+ ),
4992
+ () => {
4993
+ }
4994
+ // Intentionally ignore errors after timeout
4995
+ ).catch(() => {
4804
4996
  });
4805
4997
  reject(
4806
4998
  new ComponentStopTimeoutError({
@@ -4819,9 +5011,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4819
5011
  this.stalledComponents.delete(name);
4820
5012
  this.updateStartedFlag();
4821
5013
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4822
- this.logger.info(
4823
- "Auto-detaching process signals on last component stop"
4824
- );
5014
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4825
5015
  this.detachSignals();
4826
5016
  }
4827
5017
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4844,15 +5034,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4844
5034
  const err = error instanceof Error ? error : new Error(String(error));
4845
5035
  this.componentErrors.set(name, err);
4846
5036
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4847
- this.logger.entity(name).warn("Graceful shutdown timed out");
5037
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4848
5038
  this.lifecycleEvents.componentStopTimeout(name, err, {
4849
5039
  timeoutMS,
4850
- reason: "Graceful shutdown timed out"
5040
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4851
5041
  });
4852
5042
  return {
4853
5043
  success: false,
4854
5044
  componentName: name,
4855
- reason: "Graceful shutdown timed out",
5045
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4856
5046
  code: "component_shutdown_timeout",
4857
5047
  error: err,
4858
5048
  status: this.getComponentStatus(name)
@@ -4895,8 +5085,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4895
5085
  gracefulTimedOut: context.gracefulTimedOut
4896
5086
  }
4897
5087
  });
4898
- const timeoutMS = component.shutdownForceTimeoutMS;
4899
- let timeoutHandle;
4900
5088
  if (!component.onShutdownForce) {
4901
5089
  const stallInfo = {
4902
5090
  name,
@@ -4930,6 +5118,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4930
5118
  status: this.getComponentStatus(name)
4931
5119
  };
4932
5120
  }
5121
+ const timeoutMS = component.shutdownForceTimeoutMS;
5122
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5123
+ let timeoutHandle;
4933
5124
  try {
4934
5125
  const forcePromise = component.onShutdownForce();
4935
5126
  if (timeoutMS > 0) {
@@ -4948,23 +5139,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4948
5139
  );
4949
5140
  }
4950
5141
  }
4951
- Promise.resolve(forcePromise).catch(() => {
5142
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? (0, import_ulid2.ulid)();
5143
+ Promise.resolve(forcePromise).then(
5144
+ () => this.handleLateStopResolution(
5145
+ name,
5146
+ forceAttemptToken,
5147
+ "force"
5148
+ ),
5149
+ () => {
5150
+ }
5151
+ // Intentionally ignore errors after timeout
5152
+ ).catch(() => {
4952
5153
  });
4953
- reject(new Error("Force shutdown timed out"));
5154
+ reject(
5155
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5156
+ );
4954
5157
  }, timeoutMS);
4955
5158
  });
4956
- await Promise.race([forcePromise, timeoutPromise]);
5159
+ await Promise.race([
5160
+ forcePromise,
5161
+ timeoutPromise,
5162
+ stoppedDuringForcePromise
5163
+ ]);
4957
5164
  } else {
4958
- await forcePromise;
5165
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5166
+ }
5167
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5168
+ return {
5169
+ success: true,
5170
+ componentName: name,
5171
+ status: this.getComponentStatus(name)
5172
+ };
4959
5173
  }
4960
5174
  this.componentStates.set(name, "stopped");
4961
5175
  this.runningComponents.delete(name);
4962
5176
  this.stalledComponents.delete(name);
4963
5177
  this.updateStartedFlag();
4964
5178
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4965
- this.logger.info(
4966
- "Auto-detaching process signals on last component stop"
4967
- );
5179
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4968
5180
  this.detachSignals();
4969
5181
  }
4970
5182
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4985,8 +5197,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4985
5197
  status: this.getComponentStatus(name)
4986
5198
  };
4987
5199
  } catch (error) {
5200
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5201
+ return {
5202
+ success: true,
5203
+ componentName: name,
5204
+ status: this.getComponentStatus(name)
5205
+ };
5206
+ }
4988
5207
  const err = error instanceof Error ? error : new Error(String(error));
4989
- const isTimeout = err.message === "Force shutdown timed out";
5208
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4990
5209
  const stallInfo = {
4991
5210
  name,
4992
5211
  phase: "force",
@@ -5017,12 +5236,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5017
5236
  return {
5018
5237
  success: false,
5019
5238
  componentName: name,
5020
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5239
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
5021
5240
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
5022
5241
  error: err,
5023
5242
  status: this.getComponentStatus(name)
5024
5243
  };
5025
5244
  } finally {
5245
+ cleanupForceWaiter();
5026
5246
  if (timeoutHandle) {
5027
5247
  clearTimeout(timeoutHandle);
5028
5248
  }
@@ -5099,7 +5319,9 @@ var LifecycleManager = class extends EventEmitterProtected {
5099
5319
  "Failed to stop component during rollback, continuing: {{error.message}}",
5100
5320
  {
5101
5321
  params: {
5102
- error: result.error || new Error(result.reason || "Unknown error")
5322
+ error: result.error || new Error(
5323
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5324
+ )
5103
5325
  }
5104
5326
  }
5105
5327
  );
@@ -5113,10 +5335,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5113
5335
  }
5114
5336
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
5115
5337
  this.attachSignals();
5338
+ if (this.isStarting) {
5339
+ this.autoAttachedSignalsDuringStartup = true;
5340
+ }
5116
5341
  return true;
5117
5342
  }
5118
5343
  autoDetachSignalsIfIdle(trigger) {
5119
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5344
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5120
5345
  return;
5121
5346
  }
5122
5347
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -5158,6 +5383,210 @@ var LifecycleManager = class extends EventEmitterProtected {
5158
5383
  }).catch(() => {
5159
5384
  });
5160
5385
  }
5386
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5387
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5388
+ return { startedComponents: [...startedComponents] };
5389
+ }
5390
+ const remainingStartedComponents = [];
5391
+ let requiredFailure;
5392
+ for (const name of startedComponents) {
5393
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5394
+ if (startupStopError === void 0) {
5395
+ remainingStartedComponents.push(name);
5396
+ continue;
5397
+ }
5398
+ this.unexpectedStopsDuringStartup.delete(name);
5399
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5400
+ const component = this.getComponent(name);
5401
+ if (component?.isOptional()) {
5402
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5403
+ failedOptionalComponents.push({ name, error });
5404
+ }
5405
+ this.logger.entity(name).warn(
5406
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5407
+ {
5408
+ params: { error }
5409
+ }
5410
+ );
5411
+ continue;
5412
+ }
5413
+ this.logger.entity(name).error(
5414
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5415
+ {
5416
+ params: { error }
5417
+ }
5418
+ );
5419
+ requiredFailure ??= { name, error };
5420
+ }
5421
+ return {
5422
+ startedComponents: remainingStartedComponents,
5423
+ requiredFailure
5424
+ };
5425
+ }
5426
+ /**
5427
+ * Issues and returns a unique stop attempt token for a component.
5428
+ *
5429
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5430
+ * The late-resolution handler captures this token in its closure so it can
5431
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5432
+ * force-retry that also timed out after the original graceful promise floated
5433
+ * in the background.
5434
+ */
5435
+ issueStopAttemptToken(name) {
5436
+ const next = (0, import_ulid2.ulid)();
5437
+ this.componentStopAttemptTokens.set(name, next);
5438
+ return next;
5439
+ }
5440
+ createPendingForceStopWaiter(name) {
5441
+ let isResolved = false;
5442
+ let waiters = this.pendingForceStopWaiters.get(name);
5443
+ if (!waiters) {
5444
+ waiters = /* @__PURE__ */ new Set();
5445
+ this.pendingForceStopWaiters.set(name, waiters);
5446
+ }
5447
+ let resolveWaiter;
5448
+ const promise = new Promise((resolve) => {
5449
+ resolveWaiter = () => {
5450
+ if (isResolved) {
5451
+ return;
5452
+ }
5453
+ isResolved = true;
5454
+ resolve();
5455
+ };
5456
+ });
5457
+ waiters.add(resolveWaiter);
5458
+ return {
5459
+ promise,
5460
+ cleanup: () => {
5461
+ const pending = this.pendingForceStopWaiters.get(name);
5462
+ if (!pending) {
5463
+ return;
5464
+ }
5465
+ pending.delete(resolveWaiter);
5466
+ if (pending.size === 0) {
5467
+ this.pendingForceStopWaiters.delete(name);
5468
+ }
5469
+ }
5470
+ };
5471
+ }
5472
+ resolvePendingForceStopWaiters(name) {
5473
+ const waiters = this.pendingForceStopWaiters.get(name);
5474
+ if (!waiters || waiters.size === 0) {
5475
+ return;
5476
+ }
5477
+ this.pendingForceStopWaiters.delete(name);
5478
+ for (const resolve of waiters) {
5479
+ resolve();
5480
+ }
5481
+ }
5482
+ /**
5483
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5484
+ *
5485
+ * Usually this means a previously stalled component's original stop() or
5486
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5487
+ * stall and transition the component to stopped without a manual retry.
5488
+ *
5489
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5490
+ * after the graceful timeout but before onShutdownForce() itself times out.
5491
+ * In that window no stall entry exists yet, but the component still finished
5492
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5493
+ * path observe the already-stopped state and no-op. This overlap fix is
5494
+ * scoped to the same stop token and will not cross a later retry attempt.
5495
+ *
5496
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5497
+ *
5498
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5499
+ * force-retry) has started since this promise was launched, its token
5500
+ * won't match and we bail out immediately.
5501
+ *
5502
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5503
+ * already cleared by another path, there will be neither a matching stall
5504
+ * entry nor the force-phase overlap state, so we bail out.
5505
+ */
5506
+ handleLateStopResolution(name, token, source) {
5507
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5508
+ return;
5509
+ }
5510
+ const currentState = this.componentStates.get(name);
5511
+ const stallInfo = this.stalledComponents.get(name);
5512
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5513
+ if (stallInfo && currentState !== "stalled") {
5514
+ this.stalledComponents.delete(name);
5515
+ return;
5516
+ }
5517
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5518
+ return;
5519
+ }
5520
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5521
+ if (stallInfo) {
5522
+ this.stalledComponents.delete(name);
5523
+ }
5524
+ this.componentStates.set(name, "stopped");
5525
+ this.runningComponents.delete(name);
5526
+ this.componentErrors.set(name, null);
5527
+ this.updateStartedFlag();
5528
+ this.resolvePendingForceStopWaiters(name);
5529
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5530
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5531
+ this.detachSignals();
5532
+ }
5533
+ const timestamps = this.componentTimestamps.get(name) ?? {
5534
+ startedAt: null,
5535
+ stoppedAt: null
5536
+ };
5537
+ timestamps.stoppedAt = Date.now();
5538
+ this.componentTimestamps.set(name, timestamps);
5539
+ this.logger.entity(name).info(
5540
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5541
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5542
+ );
5543
+ if (source === "force") {
5544
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5545
+ }
5546
+ if (stallInfo && stalledDurationMS !== void 0) {
5547
+ this.lifecycleEvents.componentStalledResolved(
5548
+ name,
5549
+ stallInfo,
5550
+ stalledDurationMS
5551
+ );
5552
+ }
5553
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5554
+ }
5555
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5556
+ const currentState = this.componentStates.get(name);
5557
+ if (
5558
+ // Startup-time self-stops are valid too: start() may still be awaiting
5559
+ // some async work while an internal listener has already observed that
5560
+ // the component died and reported it.
5561
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5562
+ ) {
5563
+ return false;
5564
+ }
5565
+ this.runningComponents.delete(name);
5566
+ this.componentStates.set(name, "stopped");
5567
+ this.componentErrors.set(name, error ?? null);
5568
+ if (this.isStarting) {
5569
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5570
+ }
5571
+ this.updateStartedFlag();
5572
+ if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
5573
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5574
+ this.detachSignals();
5575
+ }
5576
+ const timestamps = this.componentTimestamps.get(name) ?? {
5577
+ startedAt: null,
5578
+ stoppedAt: null
5579
+ };
5580
+ timestamps.stoppedAt = Date.now();
5581
+ this.componentTimestamps.set(name, timestamps);
5582
+ this.logger.entity(name).warn(
5583
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5584
+ { params: { error } }
5585
+ );
5586
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5587
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5588
+ return true;
5589
+ }
5161
5590
  /**
5162
5591
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
5163
5592
  */
@@ -5729,105 +6158,87 @@ var LifecycleManager = class extends EventEmitterProtected {
5729
6158
  }
5730
6159
  }
5731
6160
  /**
5732
- * Handle reload request - calls custom callback or broadcasts to components.
6161
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6162
+ * emits the signal event, then either invokes the user-supplied callback
6163
+ * (passing the broadcast function so the user controls when/whether to
6164
+ * broadcast) or broadcasts directly when no callback is configured.
5733
6165
  *
5734
6166
  * When called from signal handlers (source='signal'), the Promise is started
5735
- * but not awaited due to Node.js signal handler constraints. Components are
5736
- * still notified and the work completes, but return values are not accessible.
5737
- *
6167
+ * but not awaited Node.js signal handlers cannot return values, so results
6168
+ * are not accessible. Components are still notified and the work completes.
5738
6169
  * When called from manual triggers (source='trigger'), the Promise is awaited
5739
6170
  * and results are returned for programmatic use.
5740
- *
5741
- * @param source - Whether triggered from signal manager or manual trigger
5742
6171
  */
5743
- async handleReloadRequest(source = "trigger") {
5744
- this.logger.info("Reload request received", { params: { source } });
5745
- this.lifecycleEvents.signalReload();
5746
- if (this.onReloadRequested) {
5747
- const broadcastFn = () => this.broadcastReload();
5748
- const result = this.onReloadRequested(broadcastFn);
6172
+ async handleSignalRequest(descriptor, source) {
6173
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6174
+ descriptor.emitSignal();
6175
+ if (descriptor.customCallback) {
6176
+ const result = descriptor.customCallback(descriptor.broadcast);
5749
6177
  if (isPromise(result)) {
5750
6178
  await result;
5751
6179
  }
5752
6180
  return {
5753
- signal: "reload",
6181
+ signal: descriptor.signal,
5754
6182
  results: [],
5755
6183
  timedOut: false,
5756
6184
  code: "ok"
5757
6185
  };
5758
6186
  }
5759
- return this.broadcastReload();
6187
+ return descriptor.broadcast();
6188
+ }
6189
+ async handleReloadRequest(source = "trigger") {
6190
+ return this.handleSignalRequest(
6191
+ {
6192
+ signal: "reload",
6193
+ dispatchedLogLabel: "Reload dispatched",
6194
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6195
+ customCallback: this.onReloadRequested,
6196
+ broadcast: () => this.broadcastReload()
6197
+ },
6198
+ source
6199
+ );
5760
6200
  }
5761
- /**
5762
- * Handle info request - calls custom callback or broadcasts to components.
5763
- *
5764
- * When called from signal handlers, the Promise executes but return values
5765
- * are not accessible due to Node.js signal handler constraints.
5766
- *
5767
- * @param source - Whether triggered from signal manager or manual trigger
5768
- */
5769
6201
  async handleInfoRequest(source = "trigger") {
5770
- this.logger.info("Info request received", { params: { source } });
5771
- this.lifecycleEvents.signalInfo();
5772
- if (this.onInfoRequested) {
5773
- const broadcastFn = () => this.broadcastInfo();
5774
- const result = this.onInfoRequested(broadcastFn);
5775
- if (isPromise(result)) {
5776
- await result;
5777
- }
5778
- return {
6202
+ return this.handleSignalRequest(
6203
+ {
5779
6204
  signal: "info",
5780
- results: [],
5781
- timedOut: false,
5782
- code: "ok"
5783
- };
5784
- }
5785
- return this.broadcastInfo();
6205
+ dispatchedLogLabel: "Info dispatched",
6206
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6207
+ customCallback: this.onInfoRequested,
6208
+ broadcast: () => this.broadcastInfo()
6209
+ },
6210
+ source
6211
+ );
5786
6212
  }
5787
- /**
5788
- * Handle debug request - calls custom callback or broadcasts to components.
5789
- *
5790
- * When called from signal handlers, the Promise executes but return values
5791
- * are not accessible due to Node.js signal handler constraints.
5792
- *
5793
- * @param source - Whether triggered from signal manager or manual trigger
5794
- */
5795
6213
  async handleDebugRequest(source = "trigger") {
5796
- this.logger.info("Debug request received", { params: { source } });
5797
- this.lifecycleEvents.signalDebug();
5798
- if (this.onDebugRequested) {
5799
- const broadcastFn = () => this.broadcastDebug();
5800
- const result = this.onDebugRequested(broadcastFn);
5801
- if (isPromise(result)) {
5802
- await result;
5803
- }
5804
- return {
6214
+ return this.handleSignalRequest(
6215
+ {
5805
6216
  signal: "debug",
5806
- results: [],
5807
- timedOut: false,
5808
- code: "ok"
5809
- };
5810
- }
5811
- return this.broadcastDebug();
6217
+ dispatchedLogLabel: "Debug dispatched",
6218
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6219
+ customCallback: this.onDebugRequested,
6220
+ broadcast: () => this.broadcastDebug()
6221
+ },
6222
+ source
6223
+ );
5812
6224
  }
5813
6225
  /**
5814
- * Broadcast reload signal to all running components.
5815
- * Calls onReload() on components that implement it.
5816
- * Continues on errors - collects all results.
6226
+ * Shared signal broadcast pipeline used by reload/info/debug.
6227
+ * Iterates running components, runs the picked handler with timeout, and
6228
+ * aggregates per-component results into a SignalBroadcastResult.
5817
6229
  */
5818
- async broadcastReload() {
6230
+ async runSignalBroadcast(descriptor) {
5819
6231
  const results = [];
5820
- const componentsToReload = this.components.filter(
6232
+ const targets = this.components.filter(
5821
6233
  (component) => this.runningComponents.has(component.getName())
5822
6234
  );
5823
6235
  if (this.isStarting) {
5824
- this.logger.info(
5825
- "Reload during startup: only reloading already-started components"
5826
- );
6236
+ this.logger.info(descriptor.startupLog);
5827
6237
  }
5828
- for (const component of componentsToReload) {
6238
+ for (const component of targets) {
5829
6239
  const name = component.getName();
5830
- if (!component.onReload) {
6240
+ const handler = descriptor.pickHandler(component);
6241
+ if (!handler) {
5831
6242
  results.push({
5832
6243
  name,
5833
6244
  called: false,
@@ -5837,13 +6248,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5837
6248
  });
5838
6249
  continue;
5839
6250
  }
5840
- this.lifecycleEvents.componentReloadStarted(name);
6251
+ descriptor.emitStarted(name);
5841
6252
  const timeoutMS = component.signalTimeoutMS;
5842
6253
  let timeoutHandle;
5843
6254
  const timeoutResult = { timedOut: true };
5844
6255
  try {
5845
- const result = component.onReload();
5846
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6256
+ const handlerResult = handler();
6257
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5847
6258
  const outcome = timeoutMS > 0 ? await Promise.race([
5848
6259
  handlerPromise,
5849
6260
  new Promise((resolve) => {
@@ -5853,7 +6264,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5853
6264
  })
5854
6265
  ]) : await handlerPromise;
5855
6266
  if (outcome === timeoutResult) {
5856
- this.logger.entity(name).warn("Reload handler timed out", {
6267
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5857
6268
  params: { timeoutMS }
5858
6269
  });
5859
6270
  Promise.resolve(handlerPromise).catch(() => {
@@ -5866,7 +6277,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5866
6277
  code: "timeout"
5867
6278
  });
5868
6279
  } else {
5869
- this.lifecycleEvents.componentReloadCompleted(name);
6280
+ descriptor.emitCompleted(name);
5870
6281
  results.push({
5871
6282
  name,
5872
6283
  called: true,
@@ -5877,10 +6288,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5877
6288
  }
5878
6289
  } catch (error) {
5879
6290
  const err = error instanceof Error ? error : new Error(String(error));
5880
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6291
+ this.logger.entity(name).error(descriptor.errorLog, {
5881
6292
  params: { error: err }
5882
6293
  });
5883
- this.lifecycleEvents.componentReloadFailed(name, err);
6294
+ descriptor.emitFailed(name, err);
5884
6295
  results.push({
5885
6296
  name,
5886
6297
  called: true,
@@ -5901,108 +6312,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5901
6312
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5902
6313
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5903
6314
  return {
5904
- signal: "reload",
6315
+ signal: descriptor.signal,
5905
6316
  results,
5906
6317
  timedOut: hasTimeout,
5907
6318
  code
5908
6319
  };
5909
6320
  }
6321
+ /**
6322
+ * Broadcast reload signal to all running components.
6323
+ * Calls onReload() on components that implement it.
6324
+ * Continues on errors - collects all results.
6325
+ */
6326
+ async broadcastReload() {
6327
+ return this.runSignalBroadcast({
6328
+ signal: "reload",
6329
+ pickHandler: (component) => component.onReload?.bind(component),
6330
+ startupLog: "Reload during startup: only reloading already-started components",
6331
+ timeoutLog: "Reload handler timed out",
6332
+ errorLog: "Reload failed: {{error.message}}",
6333
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6334
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6335
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6336
+ });
6337
+ }
5910
6338
  /**
5911
6339
  * Broadcast info signal to all running components.
5912
6340
  * Calls onInfo() on components that implement it.
5913
6341
  * Continues on errors - collects all results.
5914
6342
  */
5915
6343
  async broadcastInfo() {
5916
- const results = [];
5917
- const componentsToNotify = this.components.filter(
5918
- (component) => this.runningComponents.has(component.getName())
5919
- );
5920
- if (this.isStarting) {
5921
- this.logger.info(
5922
- "Info during startup: only notifying already-started components"
5923
- );
5924
- }
5925
- for (const component of componentsToNotify) {
5926
- const name = component.getName();
5927
- if (!component.onInfo) {
5928
- results.push({
5929
- name,
5930
- called: false,
5931
- error: null,
5932
- timedOut: false,
5933
- code: "no_handler"
5934
- });
5935
- continue;
5936
- }
5937
- this.lifecycleEvents.componentInfoStarted(name);
5938
- const timeoutMS = component.signalTimeoutMS;
5939
- let timeoutHandle;
5940
- const timeoutResult = { timedOut: true };
5941
- try {
5942
- const result = component.onInfo();
5943
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5944
- const outcome = timeoutMS > 0 ? await Promise.race([
5945
- handlerPromise,
5946
- new Promise((resolve) => {
5947
- timeoutHandle = setTimeout(() => {
5948
- resolve(timeoutResult);
5949
- }, timeoutMS);
5950
- })
5951
- ]) : await handlerPromise;
5952
- if (outcome === timeoutResult) {
5953
- this.logger.entity(name).warn("Info handler timed out", {
5954
- params: { timeoutMS }
5955
- });
5956
- Promise.resolve(handlerPromise).catch(() => {
5957
- });
5958
- results.push({
5959
- name,
5960
- called: true,
5961
- error: null,
5962
- timedOut: true,
5963
- code: "timeout"
5964
- });
5965
- } else {
5966
- this.lifecycleEvents.componentInfoCompleted(name);
5967
- results.push({
5968
- name,
5969
- called: true,
5970
- error: null,
5971
- timedOut: false,
5972
- code: "called"
5973
- });
5974
- }
5975
- } catch (error) {
5976
- const err = error instanceof Error ? error : new Error(String(error));
5977
- this.logger.entity(name).error("Info handler failed: {{error.message}}", {
5978
- params: { error: err }
5979
- });
5980
- this.lifecycleEvents.componentInfoFailed(name, err);
5981
- results.push({
5982
- name,
5983
- called: true,
5984
- error: err,
5985
- timedOut: false,
5986
- code: "error"
5987
- });
5988
- } finally {
5989
- if (timeoutHandle) {
5990
- clearTimeout(timeoutHandle);
5991
- }
5992
- }
5993
- }
5994
- const calledResults = results.filter((result) => result.called);
5995
- const hasError = calledResults.some((result) => result.error);
5996
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5997
- const hasTimeout = calledResults.some((result) => result.timedOut);
5998
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5999
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
6000
- return {
6344
+ return this.runSignalBroadcast({
6001
6345
  signal: "info",
6002
- results,
6003
- timedOut: hasTimeout,
6004
- code
6005
- };
6346
+ pickHandler: (component) => component.onInfo?.bind(component),
6347
+ startupLog: "Info during startup: only notifying already-started components",
6348
+ timeoutLog: "Info handler timed out",
6349
+ errorLog: "Info handler failed: {{error.message}}",
6350
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6351
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6352
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6353
+ });
6006
6354
  }
6007
6355
  /**
6008
6356
  * Broadcast debug signal to all running components.
@@ -6010,96 +6358,16 @@ var LifecycleManager = class extends EventEmitterProtected {
6010
6358
  * Continues on errors - collects all results.
6011
6359
  */
6012
6360
  async broadcastDebug() {
6013
- const results = [];
6014
- const componentsToNotify = this.components.filter(
6015
- (component) => this.runningComponents.has(component.getName())
6016
- );
6017
- if (this.isStarting) {
6018
- this.logger.info(
6019
- "Debug during startup: only notifying already-started components"
6020
- );
6021
- }
6022
- for (const component of componentsToNotify) {
6023
- const name = component.getName();
6024
- if (!component.onDebug) {
6025
- results.push({
6026
- name,
6027
- called: false,
6028
- error: null,
6029
- timedOut: false,
6030
- code: "no_handler"
6031
- });
6032
- continue;
6033
- }
6034
- this.lifecycleEvents.componentDebugStarted(name);
6035
- const timeoutMS = component.signalTimeoutMS;
6036
- let timeoutHandle;
6037
- const timeoutResult = { timedOut: true };
6038
- try {
6039
- const result = component.onDebug();
6040
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6041
- const outcome = timeoutMS > 0 ? await Promise.race([
6042
- handlerPromise,
6043
- new Promise((resolve) => {
6044
- timeoutHandle = setTimeout(() => {
6045
- resolve(timeoutResult);
6046
- }, timeoutMS);
6047
- })
6048
- ]) : await handlerPromise;
6049
- if (outcome === timeoutResult) {
6050
- this.logger.entity(name).warn("Debug handler timed out", {
6051
- params: { timeoutMS }
6052
- });
6053
- Promise.resolve(handlerPromise).catch(() => {
6054
- });
6055
- results.push({
6056
- name,
6057
- called: true,
6058
- error: null,
6059
- timedOut: true,
6060
- code: "timeout"
6061
- });
6062
- } else {
6063
- this.lifecycleEvents.componentDebugCompleted(name);
6064
- results.push({
6065
- name,
6066
- called: true,
6067
- error: null,
6068
- timedOut: false,
6069
- code: "called"
6070
- });
6071
- }
6072
- } catch (error) {
6073
- const err = error instanceof Error ? error : new Error(String(error));
6074
- this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
6075
- params: { error: err }
6076
- });
6077
- this.lifecycleEvents.componentDebugFailed(name, err);
6078
- results.push({
6079
- name,
6080
- called: true,
6081
- error: err,
6082
- timedOut: false,
6083
- code: "error"
6084
- });
6085
- } finally {
6086
- if (timeoutHandle) {
6087
- clearTimeout(timeoutHandle);
6088
- }
6089
- }
6090
- }
6091
- const calledResults = results.filter((result) => result.called);
6092
- const hasError = calledResults.some((result) => result.error);
6093
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
6094
- const hasTimeout = calledResults.some((result) => result.timedOut);
6095
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
6096
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
6097
- return {
6361
+ return this.runSignalBroadcast({
6098
6362
  signal: "debug",
6099
- results,
6100
- timedOut: hasTimeout,
6101
- code
6102
- };
6363
+ pickHandler: (component) => component.onDebug?.bind(component),
6364
+ startupLog: "Debug during startup: only notifying already-started components",
6365
+ timeoutLog: "Debug handler timed out",
6366
+ errorLog: "Debug handler failed: {{error.message}}",
6367
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6368
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6369
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6370
+ });
6103
6371
  }
6104
6372
  };
6105
6373
 
@@ -6125,6 +6393,12 @@ var BaseComponent = class {
6125
6393
  name;
6126
6394
  /** Reference to component-scoped lifecycle (set by manager when registered) */
6127
6395
  lifecycle;
6396
+ /** @internal Set by LifecycleManager while the component is running. */
6397
+ _unexpectedStopHandler;
6398
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6399
+ _unexpectedStopGeneration = 0;
6400
+ /** @internal Flag indicating whether this component is currently registered with a LifecycleManager */
6401
+ _isRegistered = false;
6128
6402
  /**
6129
6403
  * Create a new component
6130
6404
  *
@@ -6159,6 +6433,37 @@ var BaseComponent = class {
6159
6433
  // Default if undefined/null/non-finite
6160
6434
  );
6161
6435
  }
6436
+ /** @internal Called by LifecycleManager after a successful start. */
6437
+ _setUnexpectedStopHandler(handler) {
6438
+ this._unexpectedStopGeneration += 1;
6439
+ this._unexpectedStopHandler = handler;
6440
+ const generation = this._unexpectedStopGeneration;
6441
+ this.reportUnexpectedStop = (error) => {
6442
+ if (this._unexpectedStopGeneration !== generation) {
6443
+ return false;
6444
+ }
6445
+ return this._unexpectedStopHandler?.(error) ?? false;
6446
+ };
6447
+ }
6448
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6449
+ _clearUnexpectedStopHandler() {
6450
+ this._unexpectedStopGeneration += 1;
6451
+ this._unexpectedStopHandler = void 0;
6452
+ this.reportUnexpectedStop = () => false;
6453
+ }
6454
+ /** @internal Called by LifecycleManager when registering the component. */
6455
+ _markRegistered() {
6456
+ this._isRegistered = true;
6457
+ }
6458
+ /** @internal Called by LifecycleManager when unregistering the component. */
6459
+ _markUnregistered() {
6460
+ this._isRegistered = false;
6461
+ this.lifecycle = void 0;
6462
+ }
6463
+ /** @internal Check if the component is registered with any LifecycleManager. */
6464
+ _isRegisteredWithManager() {
6465
+ return this._isRegistered;
6466
+ }
6162
6467
  /**
6163
6468
  * Get component name
6164
6469
  */
@@ -6177,6 +6482,38 @@ var BaseComponent = class {
6177
6482
  isOptional() {
6178
6483
  return this.optional;
6179
6484
  }
6485
+ /**
6486
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6487
+ * successful start so captured references from older runs go stale.
6488
+ */
6489
+ reportUnexpectedStop = () => false;
6490
+ /**
6491
+ * Get this component's own status from the manager's perspective.
6492
+ *
6493
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6494
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6495
+ *
6496
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6497
+ */
6498
+ getSelfStatus() {
6499
+ return this.lifecycle?.getComponentStatus(this.name);
6500
+ }
6501
+ /**
6502
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6503
+ *
6504
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6505
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6506
+ * from a previous run from stopping a newer run of the same component instance.
6507
+ */
6508
+ getUnexpectedStopReporter() {
6509
+ const generation = this._unexpectedStopGeneration;
6510
+ return (error) => {
6511
+ if (this._unexpectedStopGeneration !== generation) {
6512
+ return false;
6513
+ }
6514
+ return this._unexpectedStopHandler?.(error) ?? false;
6515
+ };
6516
+ }
6180
6517
  };
6181
6518
  // Annotate the CommonJS export names for ESM import in node:
6182
6519
  0 && (module.exports = {