lifecycleion 0.0.13 → 0.0.14

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,25 @@ 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
+
1452
1481
  // src/lib/process-signal-manager.ts
1453
1482
  var import_ulid = require("ulid");
1454
1483
  var import_readline = __toESM(require("readline"), 1);
@@ -1993,8 +2022,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1993
2022
  componentTimestamps = /* @__PURE__ */ new Map();
1994
2023
  componentErrors = /* @__PURE__ */ new Map();
1995
2024
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
2025
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
2026
+ // component can be unregistered and replaced by a same-name instance before
2027
+ // the old floating stop promise settles.
2028
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
2029
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
2030
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1996
2031
  // State flags
1997
2032
  isStarting = false;
2033
+ autoAttachedSignalsDuringStartup = false;
1998
2034
  isStarted = false;
1999
2035
  isShuttingDown = false;
2000
2036
  // Unique token used to detect shutdowns that happened during async start().
@@ -2143,7 +2179,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2143
2179
  */
2144
2180
  async unregisterComponent(name, options) {
2145
2181
  if (this.isStarting || this.isShuttingDown) {
2146
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2182
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2147
2183
  params: {
2148
2184
  isStarting: this.isStarting,
2149
2185
  isShuttingDown: this.isShuttingDown
@@ -2152,7 +2188,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2152
2188
  return {
2153
2189
  success: false,
2154
2190
  componentName: name,
2155
- reason: "Cannot unregister during bulk operation",
2191
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2156
2192
  code: "bulk_operation_in_progress",
2157
2193
  wasStopped: false,
2158
2194
  wasRegistered: this.hasComponent(name)
@@ -2160,11 +2196,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2160
2196
  }
2161
2197
  const component = this.getComponent(name);
2162
2198
  if (!component) {
2163
- this.logger.entity(name).warn("Component not found");
2199
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2164
2200
  return {
2165
2201
  success: false,
2166
2202
  componentName: name,
2167
- reason: "Component not found",
2203
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2168
2204
  code: "component_not_found",
2169
2205
  wasStopped: false,
2170
2206
  wasRegistered: false
@@ -2177,7 +2213,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2177
2213
  return {
2178
2214
  success: false,
2179
2215
  componentName: name,
2180
- reason: "Component is stalled",
2216
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2181
2217
  code: "stop_failed",
2182
2218
  stopFailureReason: "stalled",
2183
2219
  wasStopped: false,
@@ -2229,10 +2265,13 @@ var LifecycleManager = class extends EventEmitterProtected {
2229
2265
  wasStopped = true;
2230
2266
  }
2231
2267
  this.components = this.components.filter((c) => c.getName() !== name);
2268
+ component._clearUnexpectedStopHandler();
2232
2269
  this.componentStates.delete(name);
2233
2270
  this.componentTimestamps.delete(name);
2234
2271
  this.componentErrors.delete(name);
2235
2272
  this.componentStartAttemptTokens.delete(name);
2273
+ this.componentStopAttemptTokens.delete(name);
2274
+ this.pendingForceStopWaiters.delete(name);
2236
2275
  this.stalledComponents.delete(name);
2237
2276
  this.runningComponents.delete(name);
2238
2277
  this.updateStartedFlag();
@@ -2579,7 +2618,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2579
2618
  startedComponents: [],
2580
2619
  failedOptionalComponents: [],
2581
2620
  skippedDueToDependency: [],
2582
- reason: "Shutdown in progress",
2621
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2583
2622
  code: "shutdown_in_progress",
2584
2623
  durationMS: Date.now() - startTime
2585
2624
  };
@@ -2639,6 +2678,8 @@ var LifecycleManager = class extends EventEmitterProtected {
2639
2678
  };
2640
2679
  }
2641
2680
  this.isStarting = true;
2681
+ this.autoAttachedSignalsDuringStartup = false;
2682
+ this.unexpectedStopsDuringStartup.clear();
2642
2683
  this.resetRepeatedShutdownRequestState();
2643
2684
  this.shutdownMethod = null;
2644
2685
  this.lastShutdownResult = null;
@@ -2771,13 +2812,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2771
2812
  error: result.error,
2772
2813
  durationMS: Date.now() - startTime
2773
2814
  };
2815
+ } else if (result.code === "component_unexpected_stop") {
2816
+ this.unexpectedStopsDuringStartup.delete(name);
2817
+ const error = result.error || new Error(
2818
+ result.reason || `Component "${name}" stopped unexpectedly`
2819
+ );
2820
+ if (component.isOptional()) {
2821
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2822
+ failedOptionalComponents.push({ name, error });
2823
+ }
2824
+ this.logger.entity(name).warn(
2825
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2826
+ {
2827
+ params: { error }
2828
+ }
2829
+ );
2830
+ } else {
2831
+ this.logger.entity(name).error(
2832
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2833
+ {
2834
+ params: { error }
2835
+ }
2836
+ );
2837
+ await this.rollbackStartup(startedComponents);
2838
+ return {
2839
+ success: false,
2840
+ startedComponents: [],
2841
+ failedOptionalComponents,
2842
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2843
+ reason: error.message,
2844
+ code: "component_unexpected_stop",
2845
+ error,
2846
+ durationMS: Date.now() - startTime
2847
+ };
2848
+ }
2774
2849
  } else {
2775
2850
  if (component.isOptional()) {
2776
2851
  this.logger.entity(name).warn(
2777
2852
  "Optional component failed to start, continuing: {{error.message}}",
2778
2853
  {
2779
2854
  params: {
2780
- error: result.error || new Error(result.reason || "Unknown error")
2855
+ error: result.error || new Error(
2856
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2857
+ )
2781
2858
  }
2782
2859
  }
2783
2860
  );
@@ -2791,14 +2868,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2791
2868
  }
2792
2869
  failedOptionalComponents.push({
2793
2870
  name,
2794
- error: result.error || new Error(result.reason || "Unknown error")
2871
+ error: result.error || new Error(
2872
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2873
+ )
2795
2874
  });
2796
2875
  } else {
2797
2876
  this.logger.entity(name).error(
2798
2877
  "Required component failed to start, rolling back: {{error.message}}",
2799
2878
  {
2800
2879
  params: {
2801
- error: result.error || new Error(result.reason || "Unknown error")
2880
+ error: result.error || new Error(
2881
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2882
+ )
2802
2883
  }
2803
2884
  }
2804
2885
  );
@@ -2815,6 +2896,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2815
2896
  };
2816
2897
  }
2817
2898
  }
2899
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2900
+ startedComponents,
2901
+ failedOptionalComponents
2902
+ );
2903
+ startedComponents.splice(0, startedComponents.length);
2904
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2905
+ if (unexpectedStopResult2.requiredFailure) {
2906
+ await this.rollbackStartup(startedComponents);
2907
+ return {
2908
+ success: false,
2909
+ startedComponents: [],
2910
+ failedOptionalComponents,
2911
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2912
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2913
+ code: "component_unexpected_stop",
2914
+ error: unexpectedStopResult2.requiredFailure.error,
2915
+ durationMS: Date.now() - startTime
2916
+ };
2917
+ }
2818
2918
  }
2819
2919
  if (hasTimedOut) {
2820
2920
  const durationMS2 = Date.now() - startTime;
@@ -2838,6 +2938,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2838
2938
  code: "startup_timeout"
2839
2939
  };
2840
2940
  }
2941
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2942
+ startedComponents,
2943
+ failedOptionalComponents
2944
+ );
2945
+ startedComponents.splice(0, startedComponents.length);
2946
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2947
+ if (unexpectedStopResult.requiredFailure) {
2948
+ await this.rollbackStartup(startedComponents);
2949
+ return {
2950
+ success: false,
2951
+ startedComponents: [],
2952
+ failedOptionalComponents,
2953
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2954
+ reason: unexpectedStopResult.requiredFailure.error.message,
2955
+ code: "component_unexpected_stop",
2956
+ error: unexpectedStopResult.requiredFailure.error,
2957
+ durationMS: Date.now() - startTime
2958
+ };
2959
+ }
2841
2960
  this.updateStartedFlag();
2842
2961
  const skippedComponentsArray = [
2843
2962
  ...Array.from(skippedDueToDependency),
@@ -2869,10 +2988,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2869
2988
  if (timeoutHandle) {
2870
2989
  clearTimeout(timeoutHandle);
2871
2990
  }
2872
- if (didAutoAttachSignalsForBulkStartup) {
2991
+ this.isStarting = false;
2992
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2873
2993
  this.autoDetachSignalsIfIdle("failed bulk startup");
2874
2994
  }
2875
- this.isStarting = false;
2995
+ this.autoAttachedSignalsDuringStartup = false;
2996
+ this.unexpectedStopsDuringStartup.clear();
2876
2997
  }
2877
2998
  }
2878
2999
  /**
@@ -2936,7 +3057,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2936
3057
  return {
2937
3058
  success: false,
2938
3059
  componentName: name,
2939
- reason: "Bulk startup in progress",
3060
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2940
3061
  code: "startup_in_progress"
2941
3062
  };
2942
3063
  }
@@ -2947,7 +3068,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2947
3068
  return {
2948
3069
  success: false,
2949
3070
  componentName: name,
2950
- reason: "Shutdown in progress",
3071
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2951
3072
  code: "shutdown_in_progress"
2952
3073
  };
2953
3074
  }
@@ -2981,7 +3102,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2981
3102
  return {
2982
3103
  success: false,
2983
3104
  componentName: name,
2984
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3105
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2985
3106
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2986
3107
  };
2987
3108
  }
@@ -3163,7 +3284,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3163
3284
  if (this.isShuttingDown) {
3164
3285
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3165
3286
  this.logger.debug(
3166
- "Logger exit called during shutdown, waiting...",
3287
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3167
3288
  {
3168
3289
  params: { exitCode }
3169
3290
  }
@@ -3172,7 +3293,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3172
3293
  this.pendingLoggerExitResolve = resolve;
3173
3294
  });
3174
3295
  }
3175
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3296
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3176
3297
  params: { exitCode }
3177
3298
  });
3178
3299
  return { action: "wait" };
@@ -3262,7 +3383,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3262
3383
  return {
3263
3384
  name,
3264
3385
  healthy: false,
3265
- message: "Component not found",
3386
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3266
3387
  checkedAt: startTime,
3267
3388
  durationMS: 0,
3268
3389
  error: null,
@@ -3275,7 +3396,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3275
3396
  return {
3276
3397
  name,
3277
3398
  healthy: false,
3278
- message: isStalled ? "Component is stalled" : "Component not running",
3399
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3279
3400
  checkedAt: startTime,
3280
3401
  durationMS: Date.now() - startTime,
3281
3402
  error: null,
@@ -3491,7 +3612,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3491
3612
  result = component.onMessage(payload, from);
3492
3613
  } catch (error) {
3493
3614
  const err = error instanceof Error ? error : new Error(String(error));
3494
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3615
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3495
3616
  params: { error: err, from }
3496
3617
  });
3497
3618
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3551,7 +3672,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3551
3672
  };
3552
3673
  } catch (error) {
3553
3674
  const err = error instanceof Error ? error : new Error(String(error));
3554
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3675
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3555
3676
  params: { error: err, from, timeoutMS }
3556
3677
  });
3557
3678
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3824,7 +3945,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3824
3945
  this.lifecycleEvents.componentRegistrationRejected({
3825
3946
  name: componentName,
3826
3947
  reason: "shutdown_in_progress",
3827
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3948
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3828
3949
  registrationIndexBefore,
3829
3950
  registrationIndexAfter: registrationIndexBefore,
3830
3951
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3836,7 +3957,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3836
3957
  targetComponentName,
3837
3958
  registrationIndexBefore,
3838
3959
  code: "shutdown_in_progress",
3839
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3960
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3840
3961
  targetFound: void 0
3841
3962
  });
3842
3963
  }
@@ -3847,7 +3968,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3847
3968
  this.lifecycleEvents.componentRegistrationRejected({
3848
3969
  name: componentName,
3849
3970
  reason: "startup_in_progress",
3850
- message: "Cannot register component during startup when it is a required dependency for other components.",
3971
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3851
3972
  registrationIndexBefore,
3852
3973
  registrationIndexAfter: registrationIndexBefore,
3853
3974
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3859,7 +3980,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3859
3980
  targetComponentName,
3860
3981
  registrationIndexBefore,
3861
3982
  code: "startup_in_progress",
3862
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3983
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3863
3984
  targetFound: void 0
3864
3985
  });
3865
3986
  }
@@ -3868,7 +3989,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3868
3989
  this.lifecycleEvents.componentRegistrationRejected({
3869
3990
  name: componentName,
3870
3991
  reason: "duplicate_instance",
3871
- message: "Component instance is already registered.",
3992
+ message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3872
3993
  registrationIndexBefore,
3873
3994
  registrationIndexAfter: registrationIndexBefore,
3874
3995
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3880,7 +4001,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3880
4001
  targetComponentName,
3881
4002
  registrationIndexBefore,
3882
4003
  code: "duplicate_instance",
3883
- reason: "Component instance is already registered.",
4004
+ reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3884
4005
  targetFound: void 0
3885
4006
  });
3886
4007
  }
@@ -4194,8 +4315,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4194
4315
  const runningComponentsToStop = shutdownOrder.filter(
4195
4316
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4196
4317
  );
4197
- const stoppedComponents = [];
4198
- const stalledComponents = [];
4318
+ const stoppedComponents = /* @__PURE__ */ new Set();
4199
4319
  let hasTimedOut = false;
4200
4320
  let timeoutHandle;
4201
4321
  try {
@@ -4226,34 +4346,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4226
4346
  this.logger.entity(name).info("Stopping component");
4227
4347
  const isRunning = this.isComponentRunning(name);
4228
4348
  const isStalled = stalledComponentNames.has(name);
4349
+ const currentState = this.componentStates.get(name);
4350
+ if (currentState === "stopped") {
4351
+ stoppedComponents.add(name);
4352
+ continue;
4353
+ }
4229
4354
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4230
4355
  success: false,
4231
4356
  componentName: name,
4232
- reason: "Component is stalled",
4357
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4233
4358
  code: "component_stalled",
4234
4359
  status: this.getComponentStatus(name)
4235
4360
  } : {
4236
4361
  success: false,
4237
4362
  componentName: name,
4238
- reason: "Component not running",
4363
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4239
4364
  code: "component_not_running",
4240
4365
  status: this.getComponentStatus(name)
4241
4366
  };
4242
4367
  if (result2.success) {
4243
- stoppedComponents.push(name);
4368
+ stoppedComponents.add(name);
4244
4369
  } else {
4245
4370
  this.logger.entity(name).error(
4246
4371
  "Component failed to stop, continuing with others: {{error.message}}",
4247
4372
  {
4248
4373
  params: {
4249
- error: result2.error || new Error(result2.reason || "Unknown error")
4374
+ error: result2.error || new Error(
4375
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4376
+ )
4250
4377
  }
4251
4378
  }
4252
4379
  );
4253
- const stallInfo = this.stalledComponents.get(name);
4254
- if (stallInfo) {
4255
- stalledComponents.push(stallInfo);
4256
- }
4257
4380
  if (shouldHaltOnStall) {
4258
4381
  this.logger.warn(
4259
4382
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4269,21 +4392,32 @@ var LifecycleManager = class extends EventEmitterProtected {
4269
4392
  } else {
4270
4393
  await shutdownOperation();
4271
4394
  }
4395
+ const finalStalledNames = /* @__PURE__ */ new Set();
4396
+ for (const name of runningComponentsToStop) {
4397
+ if (this.stalledComponents.has(name)) {
4398
+ finalStalledNames.add(name);
4399
+ }
4400
+ }
4272
4401
  if (!shouldRetryStalled) {
4273
4402
  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);
4403
+ if (this.stalledComponents.has(name)) {
4404
+ finalStalledNames.add(name);
4277
4405
  }
4278
4406
  }
4279
4407
  }
4408
+ for (const name of runningComponentsToStop) {
4409
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4410
+ stoppedComponents.add(name);
4411
+ }
4412
+ }
4413
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4280
4414
  const durationMS = Date.now() - startTime;
4281
4415
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4282
4416
  this.logger[isSuccess ? "success" : "warn"](
4283
4417
  isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4284
4418
  {
4285
4419
  params: {
4286
- stopped: stoppedComponents.length,
4420
+ stopped: stoppedComponents.size,
4287
4421
  stalled: stalledComponents.length,
4288
4422
  durationMS
4289
4423
  }
@@ -4291,7 +4425,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4291
4425
  );
4292
4426
  const result = {
4293
4427
  success: isSuccess,
4294
- stoppedComponents,
4428
+ stoppedComponents: Array.from(stoppedComponents),
4295
4429
  stalledComponents,
4296
4430
  durationMS,
4297
4431
  timedOut: hasTimedOut || void 0,
@@ -4342,7 +4476,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4342
4476
  return {
4343
4477
  success: false,
4344
4478
  componentName: name,
4345
- reason: "Component not found",
4479
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4346
4480
  code: "component_not_found"
4347
4481
  };
4348
4482
  }
@@ -4353,12 +4487,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4353
4487
  return {
4354
4488
  success: false,
4355
4489
  componentName: name,
4356
- reason: "Component not running",
4490
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4357
4491
  code: "component_not_running",
4358
4492
  status: this.getComponentStatus(name)
4359
4493
  };
4360
4494
  }
4361
4495
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4496
+ if (component.onShutdownForce) {
4497
+ this.issueStopAttemptToken(name);
4498
+ }
4362
4499
  return this.shutdownComponentForce(name, component, {
4363
4500
  gracefulPhaseRan: false,
4364
4501
  gracefulTimedOut: false,
@@ -4378,7 +4515,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4378
4515
  return {
4379
4516
  success: false,
4380
4517
  componentName: name,
4381
- reason: "Shutdown in progress",
4518
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4382
4519
  code: "shutdown_in_progress"
4383
4520
  };
4384
4521
  }
@@ -4390,7 +4527,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4390
4527
  return {
4391
4528
  success: false,
4392
4529
  componentName: name,
4393
- reason: "Bulk startup in progress",
4530
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4394
4531
  code: "startup_in_progress"
4395
4532
  };
4396
4533
  }
@@ -4400,7 +4537,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4400
4537
  return {
4401
4538
  success: false,
4402
4539
  componentName: name,
4403
- reason: "Component not found",
4540
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4404
4541
  code: "component_not_found"
4405
4542
  };
4406
4543
  }
@@ -4409,7 +4546,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4409
4546
  return {
4410
4547
  success: false,
4411
4548
  componentName: name,
4412
- reason: "Component is stalled",
4549
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4413
4550
  code: "component_stalled",
4414
4551
  status: this.getComponentStatus(name)
4415
4552
  };
@@ -4473,6 +4610,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4473
4610
  const timeoutMS = component.startupTimeoutMS;
4474
4611
  const startAttemptToken = (0, import_ulid2.ulid)();
4475
4612
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4613
+ component._setUnexpectedStopHandler(
4614
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4615
+ );
4476
4616
  const shutdownTokenAtStart = this.shutdownToken;
4477
4617
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4478
4618
  let timeoutHandle;
@@ -4515,6 +4655,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4515
4655
  } else {
4516
4656
  await startPromise;
4517
4657
  }
4658
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4659
+ component._clearUnexpectedStopHandler();
4660
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4661
+ return {
4662
+ success: false,
4663
+ componentName: name,
4664
+ reason: error.message,
4665
+ code: "component_unexpected_stop",
4666
+ error,
4667
+ status: this.getComponentStatus(name)
4668
+ };
4669
+ }
4518
4670
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4519
4671
  this.componentStates.set(name, "running");
4520
4672
  this.runningComponents.add(name);
@@ -4542,6 +4694,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4542
4694
  this.componentStates.set(name, "running");
4543
4695
  this.runningComponents.add(name);
4544
4696
  this.stalledComponents.delete(name);
4697
+ if (shouldForceStalled) {
4698
+ this.issueStopAttemptToken(name);
4699
+ }
4545
4700
  this.updateStartedFlag();
4546
4701
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4547
4702
  this.autoAttachSignals("first component start");
@@ -4561,9 +4716,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4561
4716
  status: this.getComponentStatus(name)
4562
4717
  };
4563
4718
  } catch (error) {
4719
+ component._clearUnexpectedStopHandler();
4564
4720
  const err = error instanceof Error ? error : new Error(String(error));
4721
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4722
+ const unexpectedStopError = this.componentErrors.get(name);
4723
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4724
+ return {
4725
+ success: false,
4726
+ componentName: name,
4727
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4728
+ code: "component_unexpected_stop",
4729
+ error: unexpectedStopError || new Error(
4730
+ `Component "${name}" stopped unexpectedly during startup`
4731
+ ),
4732
+ status: this.getComponentStatus(name)
4733
+ };
4734
+ }
4565
4735
  this.componentErrors.set(name, err);
4566
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4736
+ if (isStartupTimeout) {
4567
4737
  this.componentStates.set(name, "starting-timed-out");
4568
4738
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4569
4739
  params: { error: err }
@@ -4608,7 +4778,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4608
4778
  return {
4609
4779
  success: false,
4610
4780
  componentName: name,
4611
- reason: "Component not found",
4781
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4612
4782
  code: "component_not_found"
4613
4783
  };
4614
4784
  }
@@ -4616,7 +4786,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4616
4786
  return {
4617
4787
  success: false,
4618
4788
  componentName: name,
4619
- reason: "Component is stalled",
4789
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4620
4790
  code: "component_stalled",
4621
4791
  status: this.getComponentStatus(name)
4622
4792
  };
@@ -4625,7 +4795,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4625
4795
  return {
4626
4796
  success: false,
4627
4797
  componentName: name,
4628
- reason: "Component not running",
4798
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4629
4799
  code: "component_not_running",
4630
4800
  status: this.getComponentStatus(name)
4631
4801
  };
@@ -4641,6 +4811,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4641
4811
  };
4642
4812
  }
4643
4813
  if (options?.forceImmediate) {
4814
+ if (component.onShutdownForce) {
4815
+ this.issueStopAttemptToken(name);
4816
+ }
4817
+ component._clearUnexpectedStopHandler();
4644
4818
  return this.shutdownComponentForce(name, component, {
4645
4819
  gracefulPhaseRan: false,
4646
4820
  gracefulTimedOut: false,
@@ -4777,9 +4951,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4777
4951
  * Calls stop() with timeout
4778
4952
  */
4779
4953
  async shutdownComponentGraceful(name, component, options) {
4954
+ component._clearUnexpectedStopHandler();
4780
4955
  this.componentStates.set(name, "stopping");
4781
4956
  this.logger.entity(name).info("Graceful shutdown started");
4782
4957
  this.lifecycleEvents.componentStopping(name);
4958
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4783
4959
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4784
4960
  let timeoutHandle;
4785
4961
  try {
@@ -4800,7 +4976,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4800
4976
  );
4801
4977
  }
4802
4978
  }
4803
- Promise.resolve(stopPromise).catch(() => {
4979
+ Promise.resolve(stopPromise).then(
4980
+ () => this.handleLateStopResolution(
4981
+ name,
4982
+ stopAttemptToken,
4983
+ "graceful"
4984
+ ),
4985
+ () => {
4986
+ }
4987
+ // Intentionally ignore errors after timeout
4988
+ ).catch(() => {
4804
4989
  });
4805
4990
  reject(
4806
4991
  new ComponentStopTimeoutError({
@@ -4819,9 +5004,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4819
5004
  this.stalledComponents.delete(name);
4820
5005
  this.updateStartedFlag();
4821
5006
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4822
- this.logger.info(
4823
- "Auto-detaching process signals on last component stop"
4824
- );
5007
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4825
5008
  this.detachSignals();
4826
5009
  }
4827
5010
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4844,15 +5027,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4844
5027
  const err = error instanceof Error ? error : new Error(String(error));
4845
5028
  this.componentErrors.set(name, err);
4846
5029
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4847
- this.logger.entity(name).warn("Graceful shutdown timed out");
5030
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4848
5031
  this.lifecycleEvents.componentStopTimeout(name, err, {
4849
5032
  timeoutMS,
4850
- reason: "Graceful shutdown timed out"
5033
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4851
5034
  });
4852
5035
  return {
4853
5036
  success: false,
4854
5037
  componentName: name,
4855
- reason: "Graceful shutdown timed out",
5038
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4856
5039
  code: "component_shutdown_timeout",
4857
5040
  error: err,
4858
5041
  status: this.getComponentStatus(name)
@@ -4895,8 +5078,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4895
5078
  gracefulTimedOut: context.gracefulTimedOut
4896
5079
  }
4897
5080
  });
4898
- const timeoutMS = component.shutdownForceTimeoutMS;
4899
- let timeoutHandle;
4900
5081
  if (!component.onShutdownForce) {
4901
5082
  const stallInfo = {
4902
5083
  name,
@@ -4930,6 +5111,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4930
5111
  status: this.getComponentStatus(name)
4931
5112
  };
4932
5113
  }
5114
+ const timeoutMS = component.shutdownForceTimeoutMS;
5115
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5116
+ let timeoutHandle;
4933
5117
  try {
4934
5118
  const forcePromise = component.onShutdownForce();
4935
5119
  if (timeoutMS > 0) {
@@ -4948,23 +5132,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4948
5132
  );
4949
5133
  }
4950
5134
  }
4951
- Promise.resolve(forcePromise).catch(() => {
5135
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? (0, import_ulid2.ulid)();
5136
+ Promise.resolve(forcePromise).then(
5137
+ () => this.handleLateStopResolution(
5138
+ name,
5139
+ forceAttemptToken,
5140
+ "force"
5141
+ ),
5142
+ () => {
5143
+ }
5144
+ // Intentionally ignore errors after timeout
5145
+ ).catch(() => {
4952
5146
  });
4953
- reject(new Error("Force shutdown timed out"));
5147
+ reject(
5148
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5149
+ );
4954
5150
  }, timeoutMS);
4955
5151
  });
4956
- await Promise.race([forcePromise, timeoutPromise]);
5152
+ await Promise.race([
5153
+ forcePromise,
5154
+ timeoutPromise,
5155
+ stoppedDuringForcePromise
5156
+ ]);
4957
5157
  } else {
4958
- await forcePromise;
5158
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5159
+ }
5160
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5161
+ return {
5162
+ success: true,
5163
+ componentName: name,
5164
+ status: this.getComponentStatus(name)
5165
+ };
4959
5166
  }
4960
5167
  this.componentStates.set(name, "stopped");
4961
5168
  this.runningComponents.delete(name);
4962
5169
  this.stalledComponents.delete(name);
4963
5170
  this.updateStartedFlag();
4964
5171
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4965
- this.logger.info(
4966
- "Auto-detaching process signals on last component stop"
4967
- );
5172
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4968
5173
  this.detachSignals();
4969
5174
  }
4970
5175
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4985,8 +5190,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4985
5190
  status: this.getComponentStatus(name)
4986
5191
  };
4987
5192
  } catch (error) {
5193
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5194
+ return {
5195
+ success: true,
5196
+ componentName: name,
5197
+ status: this.getComponentStatus(name)
5198
+ };
5199
+ }
4988
5200
  const err = error instanceof Error ? error : new Error(String(error));
4989
- const isTimeout = err.message === "Force shutdown timed out";
5201
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4990
5202
  const stallInfo = {
4991
5203
  name,
4992
5204
  phase: "force",
@@ -5017,12 +5229,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5017
5229
  return {
5018
5230
  success: false,
5019
5231
  componentName: name,
5020
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5232
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
5021
5233
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
5022
5234
  error: err,
5023
5235
  status: this.getComponentStatus(name)
5024
5236
  };
5025
5237
  } finally {
5238
+ cleanupForceWaiter();
5026
5239
  if (timeoutHandle) {
5027
5240
  clearTimeout(timeoutHandle);
5028
5241
  }
@@ -5099,7 +5312,9 @@ var LifecycleManager = class extends EventEmitterProtected {
5099
5312
  "Failed to stop component during rollback, continuing: {{error.message}}",
5100
5313
  {
5101
5314
  params: {
5102
- error: result.error || new Error(result.reason || "Unknown error")
5315
+ error: result.error || new Error(
5316
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5317
+ )
5103
5318
  }
5104
5319
  }
5105
5320
  );
@@ -5113,10 +5328,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5113
5328
  }
5114
5329
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
5115
5330
  this.attachSignals();
5331
+ if (this.isStarting) {
5332
+ this.autoAttachedSignalsDuringStartup = true;
5333
+ }
5116
5334
  return true;
5117
5335
  }
5118
5336
  autoDetachSignalsIfIdle(trigger) {
5119
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5337
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5120
5338
  return;
5121
5339
  }
5122
5340
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -5158,6 +5376,210 @@ var LifecycleManager = class extends EventEmitterProtected {
5158
5376
  }).catch(() => {
5159
5377
  });
5160
5378
  }
5379
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5380
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5381
+ return { startedComponents: [...startedComponents] };
5382
+ }
5383
+ const remainingStartedComponents = [];
5384
+ let requiredFailure;
5385
+ for (const name of startedComponents) {
5386
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5387
+ if (startupStopError === void 0) {
5388
+ remainingStartedComponents.push(name);
5389
+ continue;
5390
+ }
5391
+ this.unexpectedStopsDuringStartup.delete(name);
5392
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5393
+ const component = this.getComponent(name);
5394
+ if (component?.isOptional()) {
5395
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5396
+ failedOptionalComponents.push({ name, error });
5397
+ }
5398
+ this.logger.entity(name).warn(
5399
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5400
+ {
5401
+ params: { error }
5402
+ }
5403
+ );
5404
+ continue;
5405
+ }
5406
+ this.logger.entity(name).error(
5407
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5408
+ {
5409
+ params: { error }
5410
+ }
5411
+ );
5412
+ requiredFailure ??= { name, error };
5413
+ }
5414
+ return {
5415
+ startedComponents: remainingStartedComponents,
5416
+ requiredFailure
5417
+ };
5418
+ }
5419
+ /**
5420
+ * Issues and returns a unique stop attempt token for a component.
5421
+ *
5422
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5423
+ * The late-resolution handler captures this token in its closure so it can
5424
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5425
+ * force-retry that also timed out after the original graceful promise floated
5426
+ * in the background.
5427
+ */
5428
+ issueStopAttemptToken(name) {
5429
+ const next = (0, import_ulid2.ulid)();
5430
+ this.componentStopAttemptTokens.set(name, next);
5431
+ return next;
5432
+ }
5433
+ createPendingForceStopWaiter(name) {
5434
+ let isResolved = false;
5435
+ let waiters = this.pendingForceStopWaiters.get(name);
5436
+ if (!waiters) {
5437
+ waiters = /* @__PURE__ */ new Set();
5438
+ this.pendingForceStopWaiters.set(name, waiters);
5439
+ }
5440
+ let resolveWaiter;
5441
+ const promise = new Promise((resolve) => {
5442
+ resolveWaiter = () => {
5443
+ if (isResolved) {
5444
+ return;
5445
+ }
5446
+ isResolved = true;
5447
+ resolve();
5448
+ };
5449
+ });
5450
+ waiters.add(resolveWaiter);
5451
+ return {
5452
+ promise,
5453
+ cleanup: () => {
5454
+ const pending = this.pendingForceStopWaiters.get(name);
5455
+ if (!pending) {
5456
+ return;
5457
+ }
5458
+ pending.delete(resolveWaiter);
5459
+ if (pending.size === 0) {
5460
+ this.pendingForceStopWaiters.delete(name);
5461
+ }
5462
+ }
5463
+ };
5464
+ }
5465
+ resolvePendingForceStopWaiters(name) {
5466
+ const waiters = this.pendingForceStopWaiters.get(name);
5467
+ if (!waiters || waiters.size === 0) {
5468
+ return;
5469
+ }
5470
+ this.pendingForceStopWaiters.delete(name);
5471
+ for (const resolve of waiters) {
5472
+ resolve();
5473
+ }
5474
+ }
5475
+ /**
5476
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5477
+ *
5478
+ * Usually this means a previously stalled component's original stop() or
5479
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5480
+ * stall and transition the component to stopped without a manual retry.
5481
+ *
5482
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5483
+ * after the graceful timeout but before onShutdownForce() itself times out.
5484
+ * In that window no stall entry exists yet, but the component still finished
5485
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5486
+ * path observe the already-stopped state and no-op. This overlap fix is
5487
+ * scoped to the same stop token and will not cross a later retry attempt.
5488
+ *
5489
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5490
+ *
5491
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5492
+ * force-retry) has started since this promise was launched, its token
5493
+ * won't match and we bail out immediately.
5494
+ *
5495
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5496
+ * already cleared by another path, there will be neither a matching stall
5497
+ * entry nor the force-phase overlap state, so we bail out.
5498
+ */
5499
+ handleLateStopResolution(name, token, source) {
5500
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5501
+ return;
5502
+ }
5503
+ const currentState = this.componentStates.get(name);
5504
+ const stallInfo = this.stalledComponents.get(name);
5505
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5506
+ if (stallInfo && currentState !== "stalled") {
5507
+ this.stalledComponents.delete(name);
5508
+ return;
5509
+ }
5510
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5511
+ return;
5512
+ }
5513
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5514
+ if (stallInfo) {
5515
+ this.stalledComponents.delete(name);
5516
+ }
5517
+ this.componentStates.set(name, "stopped");
5518
+ this.runningComponents.delete(name);
5519
+ this.componentErrors.set(name, null);
5520
+ this.updateStartedFlag();
5521
+ this.resolvePendingForceStopWaiters(name);
5522
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5523
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5524
+ this.detachSignals();
5525
+ }
5526
+ const timestamps = this.componentTimestamps.get(name) ?? {
5527
+ startedAt: null,
5528
+ stoppedAt: null
5529
+ };
5530
+ timestamps.stoppedAt = Date.now();
5531
+ this.componentTimestamps.set(name, timestamps);
5532
+ this.logger.entity(name).info(
5533
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5534
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5535
+ );
5536
+ if (source === "force") {
5537
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5538
+ }
5539
+ if (stallInfo && stalledDurationMS !== void 0) {
5540
+ this.lifecycleEvents.componentStalledResolved(
5541
+ name,
5542
+ stallInfo,
5543
+ stalledDurationMS
5544
+ );
5545
+ }
5546
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5547
+ }
5548
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5549
+ const currentState = this.componentStates.get(name);
5550
+ if (
5551
+ // Startup-time self-stops are valid too: start() may still be awaiting
5552
+ // some async work while an internal listener has already observed that
5553
+ // the component died and reported it.
5554
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5555
+ ) {
5556
+ return false;
5557
+ }
5558
+ this.runningComponents.delete(name);
5559
+ this.componentStates.set(name, "stopped");
5560
+ this.componentErrors.set(name, error ?? null);
5561
+ if (this.isStarting) {
5562
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5563
+ }
5564
+ this.updateStartedFlag();
5565
+ if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
5566
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5567
+ this.detachSignals();
5568
+ }
5569
+ const timestamps = this.componentTimestamps.get(name) ?? {
5570
+ startedAt: null,
5571
+ stoppedAt: null
5572
+ };
5573
+ timestamps.stoppedAt = Date.now();
5574
+ this.componentTimestamps.set(name, timestamps);
5575
+ this.logger.entity(name).warn(
5576
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5577
+ { params: { error } }
5578
+ );
5579
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5580
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5581
+ return true;
5582
+ }
5161
5583
  /**
5162
5584
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
5163
5585
  */
@@ -5729,105 +6151,87 @@ var LifecycleManager = class extends EventEmitterProtected {
5729
6151
  }
5730
6152
  }
5731
6153
  /**
5732
- * Handle reload request - calls custom callback or broadcasts to components.
6154
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6155
+ * emits the signal event, then either invokes the user-supplied callback
6156
+ * (passing the broadcast function so the user controls when/whether to
6157
+ * broadcast) or broadcasts directly when no callback is configured.
5733
6158
  *
5734
6159
  * 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
- *
6160
+ * but not awaited Node.js signal handlers cannot return values, so results
6161
+ * are not accessible. Components are still notified and the work completes.
5738
6162
  * When called from manual triggers (source='trigger'), the Promise is awaited
5739
6163
  * and results are returned for programmatic use.
5740
- *
5741
- * @param source - Whether triggered from signal manager or manual trigger
5742
6164
  */
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);
6165
+ async handleSignalRequest(descriptor, source) {
6166
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6167
+ descriptor.emitSignal();
6168
+ if (descriptor.customCallback) {
6169
+ const result = descriptor.customCallback(descriptor.broadcast);
5749
6170
  if (isPromise(result)) {
5750
6171
  await result;
5751
6172
  }
5752
6173
  return {
5753
- signal: "reload",
6174
+ signal: descriptor.signal,
5754
6175
  results: [],
5755
6176
  timedOut: false,
5756
6177
  code: "ok"
5757
6178
  };
5758
6179
  }
5759
- return this.broadcastReload();
6180
+ return descriptor.broadcast();
6181
+ }
6182
+ async handleReloadRequest(source = "trigger") {
6183
+ return this.handleSignalRequest(
6184
+ {
6185
+ signal: "reload",
6186
+ dispatchedLogLabel: "Reload dispatched",
6187
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6188
+ customCallback: this.onReloadRequested,
6189
+ broadcast: () => this.broadcastReload()
6190
+ },
6191
+ source
6192
+ );
5760
6193
  }
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
6194
  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 {
6195
+ return this.handleSignalRequest(
6196
+ {
5779
6197
  signal: "info",
5780
- results: [],
5781
- timedOut: false,
5782
- code: "ok"
5783
- };
5784
- }
5785
- return this.broadcastInfo();
6198
+ dispatchedLogLabel: "Info dispatched",
6199
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6200
+ customCallback: this.onInfoRequested,
6201
+ broadcast: () => this.broadcastInfo()
6202
+ },
6203
+ source
6204
+ );
5786
6205
  }
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
6206
  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 {
6207
+ return this.handleSignalRequest(
6208
+ {
5805
6209
  signal: "debug",
5806
- results: [],
5807
- timedOut: false,
5808
- code: "ok"
5809
- };
5810
- }
5811
- return this.broadcastDebug();
6210
+ dispatchedLogLabel: "Debug dispatched",
6211
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6212
+ customCallback: this.onDebugRequested,
6213
+ broadcast: () => this.broadcastDebug()
6214
+ },
6215
+ source
6216
+ );
5812
6217
  }
5813
6218
  /**
5814
- * Broadcast reload signal to all running components.
5815
- * Calls onReload() on components that implement it.
5816
- * Continues on errors - collects all results.
6219
+ * Shared signal broadcast pipeline used by reload/info/debug.
6220
+ * Iterates running components, runs the picked handler with timeout, and
6221
+ * aggregates per-component results into a SignalBroadcastResult.
5817
6222
  */
5818
- async broadcastReload() {
6223
+ async runSignalBroadcast(descriptor) {
5819
6224
  const results = [];
5820
- const componentsToReload = this.components.filter(
6225
+ const targets = this.components.filter(
5821
6226
  (component) => this.runningComponents.has(component.getName())
5822
6227
  );
5823
6228
  if (this.isStarting) {
5824
- this.logger.info(
5825
- "Reload during startup: only reloading already-started components"
5826
- );
6229
+ this.logger.info(descriptor.startupLog);
5827
6230
  }
5828
- for (const component of componentsToReload) {
6231
+ for (const component of targets) {
5829
6232
  const name = component.getName();
5830
- if (!component.onReload) {
6233
+ const handler = descriptor.pickHandler(component);
6234
+ if (!handler) {
5831
6235
  results.push({
5832
6236
  name,
5833
6237
  called: false,
@@ -5837,13 +6241,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5837
6241
  });
5838
6242
  continue;
5839
6243
  }
5840
- this.lifecycleEvents.componentReloadStarted(name);
6244
+ descriptor.emitStarted(name);
5841
6245
  const timeoutMS = component.signalTimeoutMS;
5842
6246
  let timeoutHandle;
5843
6247
  const timeoutResult = { timedOut: true };
5844
6248
  try {
5845
- const result = component.onReload();
5846
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6249
+ const handlerResult = handler();
6250
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5847
6251
  const outcome = timeoutMS > 0 ? await Promise.race([
5848
6252
  handlerPromise,
5849
6253
  new Promise((resolve) => {
@@ -5853,7 +6257,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5853
6257
  })
5854
6258
  ]) : await handlerPromise;
5855
6259
  if (outcome === timeoutResult) {
5856
- this.logger.entity(name).warn("Reload handler timed out", {
6260
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5857
6261
  params: { timeoutMS }
5858
6262
  });
5859
6263
  Promise.resolve(handlerPromise).catch(() => {
@@ -5866,7 +6270,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5866
6270
  code: "timeout"
5867
6271
  });
5868
6272
  } else {
5869
- this.lifecycleEvents.componentReloadCompleted(name);
6273
+ descriptor.emitCompleted(name);
5870
6274
  results.push({
5871
6275
  name,
5872
6276
  called: true,
@@ -5877,10 +6281,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5877
6281
  }
5878
6282
  } catch (error) {
5879
6283
  const err = error instanceof Error ? error : new Error(String(error));
5880
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6284
+ this.logger.entity(name).error(descriptor.errorLog, {
5881
6285
  params: { error: err }
5882
6286
  });
5883
- this.lifecycleEvents.componentReloadFailed(name, err);
6287
+ descriptor.emitFailed(name, err);
5884
6288
  results.push({
5885
6289
  name,
5886
6290
  called: true,
@@ -5901,108 +6305,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5901
6305
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5902
6306
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5903
6307
  return {
5904
- signal: "reload",
6308
+ signal: descriptor.signal,
5905
6309
  results,
5906
6310
  timedOut: hasTimeout,
5907
6311
  code
5908
6312
  };
5909
6313
  }
6314
+ /**
6315
+ * Broadcast reload signal to all running components.
6316
+ * Calls onReload() on components that implement it.
6317
+ * Continues on errors - collects all results.
6318
+ */
6319
+ async broadcastReload() {
6320
+ return this.runSignalBroadcast({
6321
+ signal: "reload",
6322
+ pickHandler: (component) => component.onReload?.bind(component),
6323
+ startupLog: "Reload during startup: only reloading already-started components",
6324
+ timeoutLog: "Reload handler timed out",
6325
+ errorLog: "Reload failed: {{error.message}}",
6326
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6327
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6328
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6329
+ });
6330
+ }
5910
6331
  /**
5911
6332
  * Broadcast info signal to all running components.
5912
6333
  * Calls onInfo() on components that implement it.
5913
6334
  * Continues on errors - collects all results.
5914
6335
  */
5915
6336
  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 {
6337
+ return this.runSignalBroadcast({
6001
6338
  signal: "info",
6002
- results,
6003
- timedOut: hasTimeout,
6004
- code
6005
- };
6339
+ pickHandler: (component) => component.onInfo?.bind(component),
6340
+ startupLog: "Info during startup: only notifying already-started components",
6341
+ timeoutLog: "Info handler timed out",
6342
+ errorLog: "Info handler failed: {{error.message}}",
6343
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6344
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6345
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6346
+ });
6006
6347
  }
6007
6348
  /**
6008
6349
  * Broadcast debug signal to all running components.
@@ -6010,96 +6351,16 @@ var LifecycleManager = class extends EventEmitterProtected {
6010
6351
  * Continues on errors - collects all results.
6011
6352
  */
6012
6353
  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 {
6354
+ return this.runSignalBroadcast({
6098
6355
  signal: "debug",
6099
- results,
6100
- timedOut: hasTimeout,
6101
- code
6102
- };
6356
+ pickHandler: (component) => component.onDebug?.bind(component),
6357
+ startupLog: "Debug during startup: only notifying already-started components",
6358
+ timeoutLog: "Debug handler timed out",
6359
+ errorLog: "Debug handler failed: {{error.message}}",
6360
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6361
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6362
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6363
+ });
6103
6364
  }
6104
6365
  };
6105
6366
 
@@ -6125,6 +6386,10 @@ var BaseComponent = class {
6125
6386
  name;
6126
6387
  /** Reference to component-scoped lifecycle (set by manager when registered) */
6127
6388
  lifecycle;
6389
+ /** @internal Set by LifecycleManager while the component is running. */
6390
+ _unexpectedStopHandler;
6391
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6392
+ _unexpectedStopGeneration = 0;
6128
6393
  /**
6129
6394
  * Create a new component
6130
6395
  *
@@ -6159,6 +6424,24 @@ var BaseComponent = class {
6159
6424
  // Default if undefined/null/non-finite
6160
6425
  );
6161
6426
  }
6427
+ /** @internal Called by LifecycleManager after a successful start. */
6428
+ _setUnexpectedStopHandler(handler) {
6429
+ this._unexpectedStopGeneration += 1;
6430
+ this._unexpectedStopHandler = handler;
6431
+ const generation = this._unexpectedStopGeneration;
6432
+ this.reportUnexpectedStop = (error) => {
6433
+ if (this._unexpectedStopGeneration !== generation) {
6434
+ return false;
6435
+ }
6436
+ return this._unexpectedStopHandler?.(error) ?? false;
6437
+ };
6438
+ }
6439
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6440
+ _clearUnexpectedStopHandler() {
6441
+ this._unexpectedStopGeneration += 1;
6442
+ this._unexpectedStopHandler = void 0;
6443
+ this.reportUnexpectedStop = () => false;
6444
+ }
6162
6445
  /**
6163
6446
  * Get component name
6164
6447
  */
@@ -6177,6 +6460,38 @@ var BaseComponent = class {
6177
6460
  isOptional() {
6178
6461
  return this.optional;
6179
6462
  }
6463
+ /**
6464
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6465
+ * successful start so captured references from older runs go stale.
6466
+ */
6467
+ reportUnexpectedStop = () => false;
6468
+ /**
6469
+ * Get this component's own status from the manager's perspective.
6470
+ *
6471
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6472
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6473
+ *
6474
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6475
+ */
6476
+ getSelfStatus() {
6477
+ return this.lifecycle?.getComponentStatus(this.name);
6478
+ }
6479
+ /**
6480
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6481
+ *
6482
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6483
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6484
+ * from a previous run from stopping a newer run of the same component instance.
6485
+ */
6486
+ getUnexpectedStopReporter() {
6487
+ const generation = this._unexpectedStopGeneration;
6488
+ return (error) => {
6489
+ if (this._unexpectedStopGeneration !== generation) {
6490
+ return false;
6491
+ }
6492
+ return this._unexpectedStopHandler?.(error) ?? false;
6493
+ };
6494
+ }
6180
6495
  };
6181
6496
  // Annotate the CommonJS export names for ESM import in node:
6182
6497
  0 && (module.exports = {