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.
@@ -1213,6 +1213,16 @@ var LifecycleManagerEvents = class {
1213
1213
  code: info?.code
1214
1214
  });
1215
1215
  }
1216
+ componentStalledResolved(name, stallInfo, stalledDurationMS) {
1217
+ this.emit("component:stalled-resolved", {
1218
+ name,
1219
+ stallInfo,
1220
+ stalledDurationMS
1221
+ });
1222
+ }
1223
+ componentUnexpectedStop(name, error) {
1224
+ this.emit("component:unexpected-stop", { name, error });
1225
+ }
1216
1226
  componentShutdownForceCompleted(name) {
1217
1227
  this.emit("component:shutdown-force-completed", { name });
1218
1228
  }
@@ -1399,6 +1409,25 @@ var lifecycleManagerErrCodes = {
1399
1409
  StopTimeout: "StopTimeout"
1400
1410
  };
1401
1411
 
1412
+ // src/lib/lifecycle-manager/constants.ts
1413
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
1414
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
1415
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
1416
+ var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
1417
+ var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
1418
+ var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
1419
+ var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
1420
+ var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
1421
+ var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
1422
+ var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
1423
+ var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
1424
+ var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
1425
+ var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
1426
+ var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
1427
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
1428
+ var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
1429
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
1430
+
1402
1431
  // src/lib/process-signal-manager.ts
1403
1432
  import { ulid } from "ulid";
1404
1433
  import readline from "readline";
@@ -1943,8 +1972,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1943
1972
  componentTimestamps = /* @__PURE__ */ new Map();
1944
1973
  componentErrors = /* @__PURE__ */ new Map();
1945
1974
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
1975
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
1976
+ // component can be unregistered and replaced by a same-name instance before
1977
+ // the old floating stop promise settles.
1978
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
1979
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
1980
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1946
1981
  // State flags
1947
1982
  isStarting = false;
1983
+ autoAttachedSignalsDuringStartup = false;
1948
1984
  isStarted = false;
1949
1985
  isShuttingDown = false;
1950
1986
  // Unique token used to detect shutdowns that happened during async start().
@@ -2093,7 +2129,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2093
2129
  */
2094
2130
  async unregisterComponent(name, options) {
2095
2131
  if (this.isStarting || this.isShuttingDown) {
2096
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2132
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2097
2133
  params: {
2098
2134
  isStarting: this.isStarting,
2099
2135
  isShuttingDown: this.isShuttingDown
@@ -2102,7 +2138,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2102
2138
  return {
2103
2139
  success: false,
2104
2140
  componentName: name,
2105
- reason: "Cannot unregister during bulk operation",
2141
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2106
2142
  code: "bulk_operation_in_progress",
2107
2143
  wasStopped: false,
2108
2144
  wasRegistered: this.hasComponent(name)
@@ -2110,11 +2146,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2110
2146
  }
2111
2147
  const component = this.getComponent(name);
2112
2148
  if (!component) {
2113
- this.logger.entity(name).warn("Component not found");
2149
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2114
2150
  return {
2115
2151
  success: false,
2116
2152
  componentName: name,
2117
- reason: "Component not found",
2153
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2118
2154
  code: "component_not_found",
2119
2155
  wasStopped: false,
2120
2156
  wasRegistered: false
@@ -2127,7 +2163,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2127
2163
  return {
2128
2164
  success: false,
2129
2165
  componentName: name,
2130
- reason: "Component is stalled",
2166
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2131
2167
  code: "stop_failed",
2132
2168
  stopFailureReason: "stalled",
2133
2169
  wasStopped: false,
@@ -2179,10 +2215,13 @@ var LifecycleManager = class extends EventEmitterProtected {
2179
2215
  wasStopped = true;
2180
2216
  }
2181
2217
  this.components = this.components.filter((c) => c.getName() !== name);
2218
+ component._clearUnexpectedStopHandler();
2182
2219
  this.componentStates.delete(name);
2183
2220
  this.componentTimestamps.delete(name);
2184
2221
  this.componentErrors.delete(name);
2185
2222
  this.componentStartAttemptTokens.delete(name);
2223
+ this.componentStopAttemptTokens.delete(name);
2224
+ this.pendingForceStopWaiters.delete(name);
2186
2225
  this.stalledComponents.delete(name);
2187
2226
  this.runningComponents.delete(name);
2188
2227
  this.updateStartedFlag();
@@ -2529,7 +2568,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2529
2568
  startedComponents: [],
2530
2569
  failedOptionalComponents: [],
2531
2570
  skippedDueToDependency: [],
2532
- reason: "Shutdown in progress",
2571
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2533
2572
  code: "shutdown_in_progress",
2534
2573
  durationMS: Date.now() - startTime
2535
2574
  };
@@ -2589,6 +2628,8 @@ var LifecycleManager = class extends EventEmitterProtected {
2589
2628
  };
2590
2629
  }
2591
2630
  this.isStarting = true;
2631
+ this.autoAttachedSignalsDuringStartup = false;
2632
+ this.unexpectedStopsDuringStartup.clear();
2592
2633
  this.resetRepeatedShutdownRequestState();
2593
2634
  this.shutdownMethod = null;
2594
2635
  this.lastShutdownResult = null;
@@ -2721,13 +2762,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2721
2762
  error: result.error,
2722
2763
  durationMS: Date.now() - startTime
2723
2764
  };
2765
+ } else if (result.code === "component_unexpected_stop") {
2766
+ this.unexpectedStopsDuringStartup.delete(name);
2767
+ const error = result.error || new Error(
2768
+ result.reason || `Component "${name}" stopped unexpectedly`
2769
+ );
2770
+ if (component.isOptional()) {
2771
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2772
+ failedOptionalComponents.push({ name, error });
2773
+ }
2774
+ this.logger.entity(name).warn(
2775
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2776
+ {
2777
+ params: { error }
2778
+ }
2779
+ );
2780
+ } else {
2781
+ this.logger.entity(name).error(
2782
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2783
+ {
2784
+ params: { error }
2785
+ }
2786
+ );
2787
+ await this.rollbackStartup(startedComponents);
2788
+ return {
2789
+ success: false,
2790
+ startedComponents: [],
2791
+ failedOptionalComponents,
2792
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2793
+ reason: error.message,
2794
+ code: "component_unexpected_stop",
2795
+ error,
2796
+ durationMS: Date.now() - startTime
2797
+ };
2798
+ }
2724
2799
  } else {
2725
2800
  if (component.isOptional()) {
2726
2801
  this.logger.entity(name).warn(
2727
2802
  "Optional component failed to start, continuing: {{error.message}}",
2728
2803
  {
2729
2804
  params: {
2730
- error: result.error || new Error(result.reason || "Unknown error")
2805
+ error: result.error || new Error(
2806
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2807
+ )
2731
2808
  }
2732
2809
  }
2733
2810
  );
@@ -2741,14 +2818,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2741
2818
  }
2742
2819
  failedOptionalComponents.push({
2743
2820
  name,
2744
- error: result.error || new Error(result.reason || "Unknown error")
2821
+ error: result.error || new Error(
2822
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2823
+ )
2745
2824
  });
2746
2825
  } else {
2747
2826
  this.logger.entity(name).error(
2748
2827
  "Required component failed to start, rolling back: {{error.message}}",
2749
2828
  {
2750
2829
  params: {
2751
- error: result.error || new Error(result.reason || "Unknown error")
2830
+ error: result.error || new Error(
2831
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2832
+ )
2752
2833
  }
2753
2834
  }
2754
2835
  );
@@ -2765,6 +2846,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2765
2846
  };
2766
2847
  }
2767
2848
  }
2849
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2850
+ startedComponents,
2851
+ failedOptionalComponents
2852
+ );
2853
+ startedComponents.splice(0, startedComponents.length);
2854
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2855
+ if (unexpectedStopResult2.requiredFailure) {
2856
+ await this.rollbackStartup(startedComponents);
2857
+ return {
2858
+ success: false,
2859
+ startedComponents: [],
2860
+ failedOptionalComponents,
2861
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2862
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2863
+ code: "component_unexpected_stop",
2864
+ error: unexpectedStopResult2.requiredFailure.error,
2865
+ durationMS: Date.now() - startTime
2866
+ };
2867
+ }
2768
2868
  }
2769
2869
  if (hasTimedOut) {
2770
2870
  const durationMS2 = Date.now() - startTime;
@@ -2788,6 +2888,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2788
2888
  code: "startup_timeout"
2789
2889
  };
2790
2890
  }
2891
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2892
+ startedComponents,
2893
+ failedOptionalComponents
2894
+ );
2895
+ startedComponents.splice(0, startedComponents.length);
2896
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2897
+ if (unexpectedStopResult.requiredFailure) {
2898
+ await this.rollbackStartup(startedComponents);
2899
+ return {
2900
+ success: false,
2901
+ startedComponents: [],
2902
+ failedOptionalComponents,
2903
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2904
+ reason: unexpectedStopResult.requiredFailure.error.message,
2905
+ code: "component_unexpected_stop",
2906
+ error: unexpectedStopResult.requiredFailure.error,
2907
+ durationMS: Date.now() - startTime
2908
+ };
2909
+ }
2791
2910
  this.updateStartedFlag();
2792
2911
  const skippedComponentsArray = [
2793
2912
  ...Array.from(skippedDueToDependency),
@@ -2819,10 +2938,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2819
2938
  if (timeoutHandle) {
2820
2939
  clearTimeout(timeoutHandle);
2821
2940
  }
2822
- if (didAutoAttachSignalsForBulkStartup) {
2941
+ this.isStarting = false;
2942
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2823
2943
  this.autoDetachSignalsIfIdle("failed bulk startup");
2824
2944
  }
2825
- this.isStarting = false;
2945
+ this.autoAttachedSignalsDuringStartup = false;
2946
+ this.unexpectedStopsDuringStartup.clear();
2826
2947
  }
2827
2948
  }
2828
2949
  /**
@@ -2886,7 +3007,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2886
3007
  return {
2887
3008
  success: false,
2888
3009
  componentName: name,
2889
- reason: "Bulk startup in progress",
3010
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2890
3011
  code: "startup_in_progress"
2891
3012
  };
2892
3013
  }
@@ -2897,7 +3018,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2897
3018
  return {
2898
3019
  success: false,
2899
3020
  componentName: name,
2900
- reason: "Shutdown in progress",
3021
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2901
3022
  code: "shutdown_in_progress"
2902
3023
  };
2903
3024
  }
@@ -2931,7 +3052,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2931
3052
  return {
2932
3053
  success: false,
2933
3054
  componentName: name,
2934
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3055
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2935
3056
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2936
3057
  };
2937
3058
  }
@@ -3113,7 +3234,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3113
3234
  if (this.isShuttingDown) {
3114
3235
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3115
3236
  this.logger.debug(
3116
- "Logger exit called during shutdown, waiting...",
3237
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3117
3238
  {
3118
3239
  params: { exitCode }
3119
3240
  }
@@ -3122,7 +3243,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3122
3243
  this.pendingLoggerExitResolve = resolve;
3123
3244
  });
3124
3245
  }
3125
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3246
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3126
3247
  params: { exitCode }
3127
3248
  });
3128
3249
  return { action: "wait" };
@@ -3212,7 +3333,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3212
3333
  return {
3213
3334
  name,
3214
3335
  healthy: false,
3215
- message: "Component not found",
3336
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3216
3337
  checkedAt: startTime,
3217
3338
  durationMS: 0,
3218
3339
  error: null,
@@ -3225,7 +3346,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3225
3346
  return {
3226
3347
  name,
3227
3348
  healthy: false,
3228
- message: isStalled ? "Component is stalled" : "Component not running",
3349
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3229
3350
  checkedAt: startTime,
3230
3351
  durationMS: Date.now() - startTime,
3231
3352
  error: null,
@@ -3441,7 +3562,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3441
3562
  result = component.onMessage(payload, from);
3442
3563
  } catch (error) {
3443
3564
  const err = error instanceof Error ? error : new Error(String(error));
3444
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3565
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3445
3566
  params: { error: err, from }
3446
3567
  });
3447
3568
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3501,7 +3622,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3501
3622
  };
3502
3623
  } catch (error) {
3503
3624
  const err = error instanceof Error ? error : new Error(String(error));
3504
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3625
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3505
3626
  params: { error: err, from, timeoutMS }
3506
3627
  });
3507
3628
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3774,7 +3895,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3774
3895
  this.lifecycleEvents.componentRegistrationRejected({
3775
3896
  name: componentName,
3776
3897
  reason: "shutdown_in_progress",
3777
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3898
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3778
3899
  registrationIndexBefore,
3779
3900
  registrationIndexAfter: registrationIndexBefore,
3780
3901
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3786,7 +3907,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3786
3907
  targetComponentName,
3787
3908
  registrationIndexBefore,
3788
3909
  code: "shutdown_in_progress",
3789
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3910
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3790
3911
  targetFound: void 0
3791
3912
  });
3792
3913
  }
@@ -3797,7 +3918,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3797
3918
  this.lifecycleEvents.componentRegistrationRejected({
3798
3919
  name: componentName,
3799
3920
  reason: "startup_in_progress",
3800
- message: "Cannot register component during startup when it is a required dependency for other components.",
3921
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3801
3922
  registrationIndexBefore,
3802
3923
  registrationIndexAfter: registrationIndexBefore,
3803
3924
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3809,7 +3930,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3809
3930
  targetComponentName,
3810
3931
  registrationIndexBefore,
3811
3932
  code: "startup_in_progress",
3812
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3933
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3813
3934
  targetFound: void 0
3814
3935
  });
3815
3936
  }
@@ -3818,7 +3939,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3818
3939
  this.lifecycleEvents.componentRegistrationRejected({
3819
3940
  name: componentName,
3820
3941
  reason: "duplicate_instance",
3821
- message: "Component instance is already registered.",
3942
+ message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3822
3943
  registrationIndexBefore,
3823
3944
  registrationIndexAfter: registrationIndexBefore,
3824
3945
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3830,7 +3951,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3830
3951
  targetComponentName,
3831
3952
  registrationIndexBefore,
3832
3953
  code: "duplicate_instance",
3833
- reason: "Component instance is already registered.",
3954
+ reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
3834
3955
  targetFound: void 0
3835
3956
  });
3836
3957
  }
@@ -4144,8 +4265,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4144
4265
  const runningComponentsToStop = shutdownOrder.filter(
4145
4266
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4146
4267
  );
4147
- const stoppedComponents = [];
4148
- const stalledComponents = [];
4268
+ const stoppedComponents = /* @__PURE__ */ new Set();
4149
4269
  let hasTimedOut = false;
4150
4270
  let timeoutHandle;
4151
4271
  try {
@@ -4176,34 +4296,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4176
4296
  this.logger.entity(name).info("Stopping component");
4177
4297
  const isRunning = this.isComponentRunning(name);
4178
4298
  const isStalled = stalledComponentNames.has(name);
4299
+ const currentState = this.componentStates.get(name);
4300
+ if (currentState === "stopped") {
4301
+ stoppedComponents.add(name);
4302
+ continue;
4303
+ }
4179
4304
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4180
4305
  success: false,
4181
4306
  componentName: name,
4182
- reason: "Component is stalled",
4307
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4183
4308
  code: "component_stalled",
4184
4309
  status: this.getComponentStatus(name)
4185
4310
  } : {
4186
4311
  success: false,
4187
4312
  componentName: name,
4188
- reason: "Component not running",
4313
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4189
4314
  code: "component_not_running",
4190
4315
  status: this.getComponentStatus(name)
4191
4316
  };
4192
4317
  if (result2.success) {
4193
- stoppedComponents.push(name);
4318
+ stoppedComponents.add(name);
4194
4319
  } else {
4195
4320
  this.logger.entity(name).error(
4196
4321
  "Component failed to stop, continuing with others: {{error.message}}",
4197
4322
  {
4198
4323
  params: {
4199
- error: result2.error || new Error(result2.reason || "Unknown error")
4324
+ error: result2.error || new Error(
4325
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4326
+ )
4200
4327
  }
4201
4328
  }
4202
4329
  );
4203
- const stallInfo = this.stalledComponents.get(name);
4204
- if (stallInfo) {
4205
- stalledComponents.push(stallInfo);
4206
- }
4207
4330
  if (shouldHaltOnStall) {
4208
4331
  this.logger.warn(
4209
4332
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4219,21 +4342,32 @@ var LifecycleManager = class extends EventEmitterProtected {
4219
4342
  } else {
4220
4343
  await shutdownOperation();
4221
4344
  }
4345
+ const finalStalledNames = /* @__PURE__ */ new Set();
4346
+ for (const name of runningComponentsToStop) {
4347
+ if (this.stalledComponents.has(name)) {
4348
+ finalStalledNames.add(name);
4349
+ }
4350
+ }
4222
4351
  if (!shouldRetryStalled) {
4223
4352
  for (const name of stalledComponentNames) {
4224
- const stallInfo = this.stalledComponents.get(name);
4225
- if (stallInfo && !stalledComponents.some((component) => component.name === name)) {
4226
- stalledComponents.push(stallInfo);
4353
+ if (this.stalledComponents.has(name)) {
4354
+ finalStalledNames.add(name);
4227
4355
  }
4228
4356
  }
4229
4357
  }
4358
+ for (const name of runningComponentsToStop) {
4359
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4360
+ stoppedComponents.add(name);
4361
+ }
4362
+ }
4363
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4230
4364
  const durationMS = Date.now() - startTime;
4231
4365
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4232
4366
  this.logger[isSuccess ? "success" : "warn"](
4233
4367
  isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4234
4368
  {
4235
4369
  params: {
4236
- stopped: stoppedComponents.length,
4370
+ stopped: stoppedComponents.size,
4237
4371
  stalled: stalledComponents.length,
4238
4372
  durationMS
4239
4373
  }
@@ -4241,7 +4375,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4241
4375
  );
4242
4376
  const result = {
4243
4377
  success: isSuccess,
4244
- stoppedComponents,
4378
+ stoppedComponents: Array.from(stoppedComponents),
4245
4379
  stalledComponents,
4246
4380
  durationMS,
4247
4381
  timedOut: hasTimedOut || void 0,
@@ -4292,7 +4426,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4292
4426
  return {
4293
4427
  success: false,
4294
4428
  componentName: name,
4295
- reason: "Component not found",
4429
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4296
4430
  code: "component_not_found"
4297
4431
  };
4298
4432
  }
@@ -4303,12 +4437,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4303
4437
  return {
4304
4438
  success: false,
4305
4439
  componentName: name,
4306
- reason: "Component not running",
4440
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4307
4441
  code: "component_not_running",
4308
4442
  status: this.getComponentStatus(name)
4309
4443
  };
4310
4444
  }
4311
4445
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4446
+ if (component.onShutdownForce) {
4447
+ this.issueStopAttemptToken(name);
4448
+ }
4312
4449
  return this.shutdownComponentForce(name, component, {
4313
4450
  gracefulPhaseRan: false,
4314
4451
  gracefulTimedOut: false,
@@ -4328,7 +4465,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4328
4465
  return {
4329
4466
  success: false,
4330
4467
  componentName: name,
4331
- reason: "Shutdown in progress",
4468
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4332
4469
  code: "shutdown_in_progress"
4333
4470
  };
4334
4471
  }
@@ -4340,7 +4477,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4340
4477
  return {
4341
4478
  success: false,
4342
4479
  componentName: name,
4343
- reason: "Bulk startup in progress",
4480
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4344
4481
  code: "startup_in_progress"
4345
4482
  };
4346
4483
  }
@@ -4350,7 +4487,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4350
4487
  return {
4351
4488
  success: false,
4352
4489
  componentName: name,
4353
- reason: "Component not found",
4490
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4354
4491
  code: "component_not_found"
4355
4492
  };
4356
4493
  }
@@ -4359,7 +4496,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4359
4496
  return {
4360
4497
  success: false,
4361
4498
  componentName: name,
4362
- reason: "Component is stalled",
4499
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4363
4500
  code: "component_stalled",
4364
4501
  status: this.getComponentStatus(name)
4365
4502
  };
@@ -4423,6 +4560,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4423
4560
  const timeoutMS = component.startupTimeoutMS;
4424
4561
  const startAttemptToken = ulid2();
4425
4562
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4563
+ component._setUnexpectedStopHandler(
4564
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4565
+ );
4426
4566
  const shutdownTokenAtStart = this.shutdownToken;
4427
4567
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4428
4568
  let timeoutHandle;
@@ -4465,6 +4605,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4465
4605
  } else {
4466
4606
  await startPromise;
4467
4607
  }
4608
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4609
+ component._clearUnexpectedStopHandler();
4610
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4611
+ return {
4612
+ success: false,
4613
+ componentName: name,
4614
+ reason: error.message,
4615
+ code: "component_unexpected_stop",
4616
+ error,
4617
+ status: this.getComponentStatus(name)
4618
+ };
4619
+ }
4468
4620
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4469
4621
  this.componentStates.set(name, "running");
4470
4622
  this.runningComponents.add(name);
@@ -4492,6 +4644,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4492
4644
  this.componentStates.set(name, "running");
4493
4645
  this.runningComponents.add(name);
4494
4646
  this.stalledComponents.delete(name);
4647
+ if (shouldForceStalled) {
4648
+ this.issueStopAttemptToken(name);
4649
+ }
4495
4650
  this.updateStartedFlag();
4496
4651
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4497
4652
  this.autoAttachSignals("first component start");
@@ -4511,9 +4666,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4511
4666
  status: this.getComponentStatus(name)
4512
4667
  };
4513
4668
  } catch (error) {
4669
+ component._clearUnexpectedStopHandler();
4514
4670
  const err = error instanceof Error ? error : new Error(String(error));
4671
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4672
+ const unexpectedStopError = this.componentErrors.get(name);
4673
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4674
+ return {
4675
+ success: false,
4676
+ componentName: name,
4677
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4678
+ code: "component_unexpected_stop",
4679
+ error: unexpectedStopError || new Error(
4680
+ `Component "${name}" stopped unexpectedly during startup`
4681
+ ),
4682
+ status: this.getComponentStatus(name)
4683
+ };
4684
+ }
4515
4685
  this.componentErrors.set(name, err);
4516
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4686
+ if (isStartupTimeout) {
4517
4687
  this.componentStates.set(name, "starting-timed-out");
4518
4688
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4519
4689
  params: { error: err }
@@ -4558,7 +4728,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4558
4728
  return {
4559
4729
  success: false,
4560
4730
  componentName: name,
4561
- reason: "Component not found",
4731
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4562
4732
  code: "component_not_found"
4563
4733
  };
4564
4734
  }
@@ -4566,7 +4736,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4566
4736
  return {
4567
4737
  success: false,
4568
4738
  componentName: name,
4569
- reason: "Component is stalled",
4739
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4570
4740
  code: "component_stalled",
4571
4741
  status: this.getComponentStatus(name)
4572
4742
  };
@@ -4575,7 +4745,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4575
4745
  return {
4576
4746
  success: false,
4577
4747
  componentName: name,
4578
- reason: "Component not running",
4748
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4579
4749
  code: "component_not_running",
4580
4750
  status: this.getComponentStatus(name)
4581
4751
  };
@@ -4591,6 +4761,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4591
4761
  };
4592
4762
  }
4593
4763
  if (options?.forceImmediate) {
4764
+ if (component.onShutdownForce) {
4765
+ this.issueStopAttemptToken(name);
4766
+ }
4767
+ component._clearUnexpectedStopHandler();
4594
4768
  return this.shutdownComponentForce(name, component, {
4595
4769
  gracefulPhaseRan: false,
4596
4770
  gracefulTimedOut: false,
@@ -4727,9 +4901,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4727
4901
  * Calls stop() with timeout
4728
4902
  */
4729
4903
  async shutdownComponentGraceful(name, component, options) {
4904
+ component._clearUnexpectedStopHandler();
4730
4905
  this.componentStates.set(name, "stopping");
4731
4906
  this.logger.entity(name).info("Graceful shutdown started");
4732
4907
  this.lifecycleEvents.componentStopping(name);
4908
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4733
4909
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4734
4910
  let timeoutHandle;
4735
4911
  try {
@@ -4750,7 +4926,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4750
4926
  );
4751
4927
  }
4752
4928
  }
4753
- Promise.resolve(stopPromise).catch(() => {
4929
+ Promise.resolve(stopPromise).then(
4930
+ () => this.handleLateStopResolution(
4931
+ name,
4932
+ stopAttemptToken,
4933
+ "graceful"
4934
+ ),
4935
+ () => {
4936
+ }
4937
+ // Intentionally ignore errors after timeout
4938
+ ).catch(() => {
4754
4939
  });
4755
4940
  reject(
4756
4941
  new ComponentStopTimeoutError({
@@ -4769,9 +4954,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4769
4954
  this.stalledComponents.delete(name);
4770
4955
  this.updateStartedFlag();
4771
4956
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4772
- this.logger.info(
4773
- "Auto-detaching process signals on last component stop"
4774
- );
4957
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4775
4958
  this.detachSignals();
4776
4959
  }
4777
4960
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4794,15 +4977,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4794
4977
  const err = error instanceof Error ? error : new Error(String(error));
4795
4978
  this.componentErrors.set(name, err);
4796
4979
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4797
- this.logger.entity(name).warn("Graceful shutdown timed out");
4980
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4798
4981
  this.lifecycleEvents.componentStopTimeout(name, err, {
4799
4982
  timeoutMS,
4800
- reason: "Graceful shutdown timed out"
4983
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4801
4984
  });
4802
4985
  return {
4803
4986
  success: false,
4804
4987
  componentName: name,
4805
- reason: "Graceful shutdown timed out",
4988
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4806
4989
  code: "component_shutdown_timeout",
4807
4990
  error: err,
4808
4991
  status: this.getComponentStatus(name)
@@ -4845,8 +5028,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4845
5028
  gracefulTimedOut: context.gracefulTimedOut
4846
5029
  }
4847
5030
  });
4848
- const timeoutMS = component.shutdownForceTimeoutMS;
4849
- let timeoutHandle;
4850
5031
  if (!component.onShutdownForce) {
4851
5032
  const stallInfo = {
4852
5033
  name,
@@ -4880,6 +5061,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4880
5061
  status: this.getComponentStatus(name)
4881
5062
  };
4882
5063
  }
5064
+ const timeoutMS = component.shutdownForceTimeoutMS;
5065
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5066
+ let timeoutHandle;
4883
5067
  try {
4884
5068
  const forcePromise = component.onShutdownForce();
4885
5069
  if (timeoutMS > 0) {
@@ -4898,23 +5082,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4898
5082
  );
4899
5083
  }
4900
5084
  }
4901
- Promise.resolve(forcePromise).catch(() => {
5085
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? ulid2();
5086
+ Promise.resolve(forcePromise).then(
5087
+ () => this.handleLateStopResolution(
5088
+ name,
5089
+ forceAttemptToken,
5090
+ "force"
5091
+ ),
5092
+ () => {
5093
+ }
5094
+ // Intentionally ignore errors after timeout
5095
+ ).catch(() => {
4902
5096
  });
4903
- reject(new Error("Force shutdown timed out"));
5097
+ reject(
5098
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5099
+ );
4904
5100
  }, timeoutMS);
4905
5101
  });
4906
- await Promise.race([forcePromise, timeoutPromise]);
5102
+ await Promise.race([
5103
+ forcePromise,
5104
+ timeoutPromise,
5105
+ stoppedDuringForcePromise
5106
+ ]);
4907
5107
  } else {
4908
- await forcePromise;
5108
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5109
+ }
5110
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5111
+ return {
5112
+ success: true,
5113
+ componentName: name,
5114
+ status: this.getComponentStatus(name)
5115
+ };
4909
5116
  }
4910
5117
  this.componentStates.set(name, "stopped");
4911
5118
  this.runningComponents.delete(name);
4912
5119
  this.stalledComponents.delete(name);
4913
5120
  this.updateStartedFlag();
4914
5121
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4915
- this.logger.info(
4916
- "Auto-detaching process signals on last component stop"
4917
- );
5122
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4918
5123
  this.detachSignals();
4919
5124
  }
4920
5125
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4935,8 +5140,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4935
5140
  status: this.getComponentStatus(name)
4936
5141
  };
4937
5142
  } catch (error) {
5143
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5144
+ return {
5145
+ success: true,
5146
+ componentName: name,
5147
+ status: this.getComponentStatus(name)
5148
+ };
5149
+ }
4938
5150
  const err = error instanceof Error ? error : new Error(String(error));
4939
- const isTimeout = err.message === "Force shutdown timed out";
5151
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4940
5152
  const stallInfo = {
4941
5153
  name,
4942
5154
  phase: "force",
@@ -4967,12 +5179,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4967
5179
  return {
4968
5180
  success: false,
4969
5181
  componentName: name,
4970
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5182
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
4971
5183
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
4972
5184
  error: err,
4973
5185
  status: this.getComponentStatus(name)
4974
5186
  };
4975
5187
  } finally {
5188
+ cleanupForceWaiter();
4976
5189
  if (timeoutHandle) {
4977
5190
  clearTimeout(timeoutHandle);
4978
5191
  }
@@ -5049,7 +5262,9 @@ var LifecycleManager = class extends EventEmitterProtected {
5049
5262
  "Failed to stop component during rollback, continuing: {{error.message}}",
5050
5263
  {
5051
5264
  params: {
5052
- error: result.error || new Error(result.reason || "Unknown error")
5265
+ error: result.error || new Error(
5266
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5267
+ )
5053
5268
  }
5054
5269
  }
5055
5270
  );
@@ -5063,10 +5278,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5063
5278
  }
5064
5279
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
5065
5280
  this.attachSignals();
5281
+ if (this.isStarting) {
5282
+ this.autoAttachedSignalsDuringStartup = true;
5283
+ }
5066
5284
  return true;
5067
5285
  }
5068
5286
  autoDetachSignalsIfIdle(trigger) {
5069
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5287
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5070
5288
  return;
5071
5289
  }
5072
5290
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -5108,6 +5326,210 @@ var LifecycleManager = class extends EventEmitterProtected {
5108
5326
  }).catch(() => {
5109
5327
  });
5110
5328
  }
5329
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5330
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5331
+ return { startedComponents: [...startedComponents] };
5332
+ }
5333
+ const remainingStartedComponents = [];
5334
+ let requiredFailure;
5335
+ for (const name of startedComponents) {
5336
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5337
+ if (startupStopError === void 0) {
5338
+ remainingStartedComponents.push(name);
5339
+ continue;
5340
+ }
5341
+ this.unexpectedStopsDuringStartup.delete(name);
5342
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5343
+ const component = this.getComponent(name);
5344
+ if (component?.isOptional()) {
5345
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5346
+ failedOptionalComponents.push({ name, error });
5347
+ }
5348
+ this.logger.entity(name).warn(
5349
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5350
+ {
5351
+ params: { error }
5352
+ }
5353
+ );
5354
+ continue;
5355
+ }
5356
+ this.logger.entity(name).error(
5357
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5358
+ {
5359
+ params: { error }
5360
+ }
5361
+ );
5362
+ requiredFailure ??= { name, error };
5363
+ }
5364
+ return {
5365
+ startedComponents: remainingStartedComponents,
5366
+ requiredFailure
5367
+ };
5368
+ }
5369
+ /**
5370
+ * Issues and returns a unique stop attempt token for a component.
5371
+ *
5372
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5373
+ * The late-resolution handler captures this token in its closure so it can
5374
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5375
+ * force-retry that also timed out after the original graceful promise floated
5376
+ * in the background.
5377
+ */
5378
+ issueStopAttemptToken(name) {
5379
+ const next = ulid2();
5380
+ this.componentStopAttemptTokens.set(name, next);
5381
+ return next;
5382
+ }
5383
+ createPendingForceStopWaiter(name) {
5384
+ let isResolved = false;
5385
+ let waiters = this.pendingForceStopWaiters.get(name);
5386
+ if (!waiters) {
5387
+ waiters = /* @__PURE__ */ new Set();
5388
+ this.pendingForceStopWaiters.set(name, waiters);
5389
+ }
5390
+ let resolveWaiter;
5391
+ const promise = new Promise((resolve) => {
5392
+ resolveWaiter = () => {
5393
+ if (isResolved) {
5394
+ return;
5395
+ }
5396
+ isResolved = true;
5397
+ resolve();
5398
+ };
5399
+ });
5400
+ waiters.add(resolveWaiter);
5401
+ return {
5402
+ promise,
5403
+ cleanup: () => {
5404
+ const pending = this.pendingForceStopWaiters.get(name);
5405
+ if (!pending) {
5406
+ return;
5407
+ }
5408
+ pending.delete(resolveWaiter);
5409
+ if (pending.size === 0) {
5410
+ this.pendingForceStopWaiters.delete(name);
5411
+ }
5412
+ }
5413
+ };
5414
+ }
5415
+ resolvePendingForceStopWaiters(name) {
5416
+ const waiters = this.pendingForceStopWaiters.get(name);
5417
+ if (!waiters || waiters.size === 0) {
5418
+ return;
5419
+ }
5420
+ this.pendingForceStopWaiters.delete(name);
5421
+ for (const resolve of waiters) {
5422
+ resolve();
5423
+ }
5424
+ }
5425
+ /**
5426
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5427
+ *
5428
+ * Usually this means a previously stalled component's original stop() or
5429
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5430
+ * stall and transition the component to stopped without a manual retry.
5431
+ *
5432
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5433
+ * after the graceful timeout but before onShutdownForce() itself times out.
5434
+ * In that window no stall entry exists yet, but the component still finished
5435
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5436
+ * path observe the already-stopped state and no-op. This overlap fix is
5437
+ * scoped to the same stop token and will not cross a later retry attempt.
5438
+ *
5439
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5440
+ *
5441
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5442
+ * force-retry) has started since this promise was launched, its token
5443
+ * won't match and we bail out immediately.
5444
+ *
5445
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5446
+ * already cleared by another path, there will be neither a matching stall
5447
+ * entry nor the force-phase overlap state, so we bail out.
5448
+ */
5449
+ handleLateStopResolution(name, token, source) {
5450
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5451
+ return;
5452
+ }
5453
+ const currentState = this.componentStates.get(name);
5454
+ const stallInfo = this.stalledComponents.get(name);
5455
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5456
+ if (stallInfo && currentState !== "stalled") {
5457
+ this.stalledComponents.delete(name);
5458
+ return;
5459
+ }
5460
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5461
+ return;
5462
+ }
5463
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5464
+ if (stallInfo) {
5465
+ this.stalledComponents.delete(name);
5466
+ }
5467
+ this.componentStates.set(name, "stopped");
5468
+ this.runningComponents.delete(name);
5469
+ this.componentErrors.set(name, null);
5470
+ this.updateStartedFlag();
5471
+ this.resolvePendingForceStopWaiters(name);
5472
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5473
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5474
+ this.detachSignals();
5475
+ }
5476
+ const timestamps = this.componentTimestamps.get(name) ?? {
5477
+ startedAt: null,
5478
+ stoppedAt: null
5479
+ };
5480
+ timestamps.stoppedAt = Date.now();
5481
+ this.componentTimestamps.set(name, timestamps);
5482
+ this.logger.entity(name).info(
5483
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5484
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5485
+ );
5486
+ if (source === "force") {
5487
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5488
+ }
5489
+ if (stallInfo && stalledDurationMS !== void 0) {
5490
+ this.lifecycleEvents.componentStalledResolved(
5491
+ name,
5492
+ stallInfo,
5493
+ stalledDurationMS
5494
+ );
5495
+ }
5496
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5497
+ }
5498
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5499
+ const currentState = this.componentStates.get(name);
5500
+ if (
5501
+ // Startup-time self-stops are valid too: start() may still be awaiting
5502
+ // some async work while an internal listener has already observed that
5503
+ // the component died and reported it.
5504
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5505
+ ) {
5506
+ return false;
5507
+ }
5508
+ this.runningComponents.delete(name);
5509
+ this.componentStates.set(name, "stopped");
5510
+ this.componentErrors.set(name, error ?? null);
5511
+ if (this.isStarting) {
5512
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5513
+ }
5514
+ this.updateStartedFlag();
5515
+ if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
5516
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5517
+ this.detachSignals();
5518
+ }
5519
+ const timestamps = this.componentTimestamps.get(name) ?? {
5520
+ startedAt: null,
5521
+ stoppedAt: null
5522
+ };
5523
+ timestamps.stoppedAt = Date.now();
5524
+ this.componentTimestamps.set(name, timestamps);
5525
+ this.logger.entity(name).warn(
5526
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5527
+ { params: { error } }
5528
+ );
5529
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5530
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5531
+ return true;
5532
+ }
5111
5533
  /**
5112
5534
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
5113
5535
  */
@@ -5679,105 +6101,87 @@ var LifecycleManager = class extends EventEmitterProtected {
5679
6101
  }
5680
6102
  }
5681
6103
  /**
5682
- * Handle reload request - calls custom callback or broadcasts to components.
6104
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6105
+ * emits the signal event, then either invokes the user-supplied callback
6106
+ * (passing the broadcast function so the user controls when/whether to
6107
+ * broadcast) or broadcasts directly when no callback is configured.
5683
6108
  *
5684
6109
  * When called from signal handlers (source='signal'), the Promise is started
5685
- * but not awaited due to Node.js signal handler constraints. Components are
5686
- * still notified and the work completes, but return values are not accessible.
5687
- *
6110
+ * but not awaited Node.js signal handlers cannot return values, so results
6111
+ * are not accessible. Components are still notified and the work completes.
5688
6112
  * When called from manual triggers (source='trigger'), the Promise is awaited
5689
6113
  * and results are returned for programmatic use.
5690
- *
5691
- * @param source - Whether triggered from signal manager or manual trigger
5692
6114
  */
5693
- async handleReloadRequest(source = "trigger") {
5694
- this.logger.info("Reload request received", { params: { source } });
5695
- this.lifecycleEvents.signalReload();
5696
- if (this.onReloadRequested) {
5697
- const broadcastFn = () => this.broadcastReload();
5698
- const result = this.onReloadRequested(broadcastFn);
6115
+ async handleSignalRequest(descriptor, source) {
6116
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6117
+ descriptor.emitSignal();
6118
+ if (descriptor.customCallback) {
6119
+ const result = descriptor.customCallback(descriptor.broadcast);
5699
6120
  if (isPromise(result)) {
5700
6121
  await result;
5701
6122
  }
5702
6123
  return {
5703
- signal: "reload",
6124
+ signal: descriptor.signal,
5704
6125
  results: [],
5705
6126
  timedOut: false,
5706
6127
  code: "ok"
5707
6128
  };
5708
6129
  }
5709
- return this.broadcastReload();
6130
+ return descriptor.broadcast();
6131
+ }
6132
+ async handleReloadRequest(source = "trigger") {
6133
+ return this.handleSignalRequest(
6134
+ {
6135
+ signal: "reload",
6136
+ dispatchedLogLabel: "Reload dispatched",
6137
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6138
+ customCallback: this.onReloadRequested,
6139
+ broadcast: () => this.broadcastReload()
6140
+ },
6141
+ source
6142
+ );
5710
6143
  }
5711
- /**
5712
- * Handle info request - calls custom callback or broadcasts to components.
5713
- *
5714
- * When called from signal handlers, the Promise executes but return values
5715
- * are not accessible due to Node.js signal handler constraints.
5716
- *
5717
- * @param source - Whether triggered from signal manager or manual trigger
5718
- */
5719
6144
  async handleInfoRequest(source = "trigger") {
5720
- this.logger.info("Info request received", { params: { source } });
5721
- this.lifecycleEvents.signalInfo();
5722
- if (this.onInfoRequested) {
5723
- const broadcastFn = () => this.broadcastInfo();
5724
- const result = this.onInfoRequested(broadcastFn);
5725
- if (isPromise(result)) {
5726
- await result;
5727
- }
5728
- return {
6145
+ return this.handleSignalRequest(
6146
+ {
5729
6147
  signal: "info",
5730
- results: [],
5731
- timedOut: false,
5732
- code: "ok"
5733
- };
5734
- }
5735
- return this.broadcastInfo();
6148
+ dispatchedLogLabel: "Info dispatched",
6149
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6150
+ customCallback: this.onInfoRequested,
6151
+ broadcast: () => this.broadcastInfo()
6152
+ },
6153
+ source
6154
+ );
5736
6155
  }
5737
- /**
5738
- * Handle debug request - calls custom callback or broadcasts to components.
5739
- *
5740
- * When called from signal handlers, the Promise executes but return values
5741
- * are not accessible due to Node.js signal handler constraints.
5742
- *
5743
- * @param source - Whether triggered from signal manager or manual trigger
5744
- */
5745
6156
  async handleDebugRequest(source = "trigger") {
5746
- this.logger.info("Debug request received", { params: { source } });
5747
- this.lifecycleEvents.signalDebug();
5748
- if (this.onDebugRequested) {
5749
- const broadcastFn = () => this.broadcastDebug();
5750
- const result = this.onDebugRequested(broadcastFn);
5751
- if (isPromise(result)) {
5752
- await result;
5753
- }
5754
- return {
6157
+ return this.handleSignalRequest(
6158
+ {
5755
6159
  signal: "debug",
5756
- results: [],
5757
- timedOut: false,
5758
- code: "ok"
5759
- };
5760
- }
5761
- return this.broadcastDebug();
6160
+ dispatchedLogLabel: "Debug dispatched",
6161
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6162
+ customCallback: this.onDebugRequested,
6163
+ broadcast: () => this.broadcastDebug()
6164
+ },
6165
+ source
6166
+ );
5762
6167
  }
5763
6168
  /**
5764
- * Broadcast reload signal to all running components.
5765
- * Calls onReload() on components that implement it.
5766
- * Continues on errors - collects all results.
6169
+ * Shared signal broadcast pipeline used by reload/info/debug.
6170
+ * Iterates running components, runs the picked handler with timeout, and
6171
+ * aggregates per-component results into a SignalBroadcastResult.
5767
6172
  */
5768
- async broadcastReload() {
6173
+ async runSignalBroadcast(descriptor) {
5769
6174
  const results = [];
5770
- const componentsToReload = this.components.filter(
6175
+ const targets = this.components.filter(
5771
6176
  (component) => this.runningComponents.has(component.getName())
5772
6177
  );
5773
6178
  if (this.isStarting) {
5774
- this.logger.info(
5775
- "Reload during startup: only reloading already-started components"
5776
- );
6179
+ this.logger.info(descriptor.startupLog);
5777
6180
  }
5778
- for (const component of componentsToReload) {
6181
+ for (const component of targets) {
5779
6182
  const name = component.getName();
5780
- if (!component.onReload) {
6183
+ const handler = descriptor.pickHandler(component);
6184
+ if (!handler) {
5781
6185
  results.push({
5782
6186
  name,
5783
6187
  called: false,
@@ -5787,13 +6191,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5787
6191
  });
5788
6192
  continue;
5789
6193
  }
5790
- this.lifecycleEvents.componentReloadStarted(name);
6194
+ descriptor.emitStarted(name);
5791
6195
  const timeoutMS = component.signalTimeoutMS;
5792
6196
  let timeoutHandle;
5793
6197
  const timeoutResult = { timedOut: true };
5794
6198
  try {
5795
- const result = component.onReload();
5796
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6199
+ const handlerResult = handler();
6200
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5797
6201
  const outcome = timeoutMS > 0 ? await Promise.race([
5798
6202
  handlerPromise,
5799
6203
  new Promise((resolve) => {
@@ -5803,7 +6207,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5803
6207
  })
5804
6208
  ]) : await handlerPromise;
5805
6209
  if (outcome === timeoutResult) {
5806
- this.logger.entity(name).warn("Reload handler timed out", {
6210
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5807
6211
  params: { timeoutMS }
5808
6212
  });
5809
6213
  Promise.resolve(handlerPromise).catch(() => {
@@ -5816,7 +6220,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5816
6220
  code: "timeout"
5817
6221
  });
5818
6222
  } else {
5819
- this.lifecycleEvents.componentReloadCompleted(name);
6223
+ descriptor.emitCompleted(name);
5820
6224
  results.push({
5821
6225
  name,
5822
6226
  called: true,
@@ -5827,10 +6231,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5827
6231
  }
5828
6232
  } catch (error) {
5829
6233
  const err = error instanceof Error ? error : new Error(String(error));
5830
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6234
+ this.logger.entity(name).error(descriptor.errorLog, {
5831
6235
  params: { error: err }
5832
6236
  });
5833
- this.lifecycleEvents.componentReloadFailed(name, err);
6237
+ descriptor.emitFailed(name, err);
5834
6238
  results.push({
5835
6239
  name,
5836
6240
  called: true,
@@ -5851,108 +6255,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5851
6255
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5852
6256
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5853
6257
  return {
5854
- signal: "reload",
6258
+ signal: descriptor.signal,
5855
6259
  results,
5856
6260
  timedOut: hasTimeout,
5857
6261
  code
5858
6262
  };
5859
6263
  }
6264
+ /**
6265
+ * Broadcast reload signal to all running components.
6266
+ * Calls onReload() on components that implement it.
6267
+ * Continues on errors - collects all results.
6268
+ */
6269
+ async broadcastReload() {
6270
+ return this.runSignalBroadcast({
6271
+ signal: "reload",
6272
+ pickHandler: (component) => component.onReload?.bind(component),
6273
+ startupLog: "Reload during startup: only reloading already-started components",
6274
+ timeoutLog: "Reload handler timed out",
6275
+ errorLog: "Reload failed: {{error.message}}",
6276
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6277
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6278
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6279
+ });
6280
+ }
5860
6281
  /**
5861
6282
  * Broadcast info signal to all running components.
5862
6283
  * Calls onInfo() on components that implement it.
5863
6284
  * Continues on errors - collects all results.
5864
6285
  */
5865
6286
  async broadcastInfo() {
5866
- const results = [];
5867
- const componentsToNotify = this.components.filter(
5868
- (component) => this.runningComponents.has(component.getName())
5869
- );
5870
- if (this.isStarting) {
5871
- this.logger.info(
5872
- "Info during startup: only notifying already-started components"
5873
- );
5874
- }
5875
- for (const component of componentsToNotify) {
5876
- const name = component.getName();
5877
- if (!component.onInfo) {
5878
- results.push({
5879
- name,
5880
- called: false,
5881
- error: null,
5882
- timedOut: false,
5883
- code: "no_handler"
5884
- });
5885
- continue;
5886
- }
5887
- this.lifecycleEvents.componentInfoStarted(name);
5888
- const timeoutMS = component.signalTimeoutMS;
5889
- let timeoutHandle;
5890
- const timeoutResult = { timedOut: true };
5891
- try {
5892
- const result = component.onInfo();
5893
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5894
- const outcome = timeoutMS > 0 ? await Promise.race([
5895
- handlerPromise,
5896
- new Promise((resolve) => {
5897
- timeoutHandle = setTimeout(() => {
5898
- resolve(timeoutResult);
5899
- }, timeoutMS);
5900
- })
5901
- ]) : await handlerPromise;
5902
- if (outcome === timeoutResult) {
5903
- this.logger.entity(name).warn("Info handler timed out", {
5904
- params: { timeoutMS }
5905
- });
5906
- Promise.resolve(handlerPromise).catch(() => {
5907
- });
5908
- results.push({
5909
- name,
5910
- called: true,
5911
- error: null,
5912
- timedOut: true,
5913
- code: "timeout"
5914
- });
5915
- } else {
5916
- this.lifecycleEvents.componentInfoCompleted(name);
5917
- results.push({
5918
- name,
5919
- called: true,
5920
- error: null,
5921
- timedOut: false,
5922
- code: "called"
5923
- });
5924
- }
5925
- } catch (error) {
5926
- const err = error instanceof Error ? error : new Error(String(error));
5927
- this.logger.entity(name).error("Info handler failed: {{error.message}}", {
5928
- params: { error: err }
5929
- });
5930
- this.lifecycleEvents.componentInfoFailed(name, err);
5931
- results.push({
5932
- name,
5933
- called: true,
5934
- error: err,
5935
- timedOut: false,
5936
- code: "error"
5937
- });
5938
- } finally {
5939
- if (timeoutHandle) {
5940
- clearTimeout(timeoutHandle);
5941
- }
5942
- }
5943
- }
5944
- const calledResults = results.filter((result) => result.called);
5945
- const hasError = calledResults.some((result) => result.error);
5946
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
5947
- const hasTimeout = calledResults.some((result) => result.timedOut);
5948
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5949
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5950
- return {
6287
+ return this.runSignalBroadcast({
5951
6288
  signal: "info",
5952
- results,
5953
- timedOut: hasTimeout,
5954
- code
5955
- };
6289
+ pickHandler: (component) => component.onInfo?.bind(component),
6290
+ startupLog: "Info during startup: only notifying already-started components",
6291
+ timeoutLog: "Info handler timed out",
6292
+ errorLog: "Info handler failed: {{error.message}}",
6293
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6294
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6295
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6296
+ });
5956
6297
  }
5957
6298
  /**
5958
6299
  * Broadcast debug signal to all running components.
@@ -5960,96 +6301,16 @@ var LifecycleManager = class extends EventEmitterProtected {
5960
6301
  * Continues on errors - collects all results.
5961
6302
  */
5962
6303
  async broadcastDebug() {
5963
- const results = [];
5964
- const componentsToNotify = this.components.filter(
5965
- (component) => this.runningComponents.has(component.getName())
5966
- );
5967
- if (this.isStarting) {
5968
- this.logger.info(
5969
- "Debug during startup: only notifying already-started components"
5970
- );
5971
- }
5972
- for (const component of componentsToNotify) {
5973
- const name = component.getName();
5974
- if (!component.onDebug) {
5975
- results.push({
5976
- name,
5977
- called: false,
5978
- error: null,
5979
- timedOut: false,
5980
- code: "no_handler"
5981
- });
5982
- continue;
5983
- }
5984
- this.lifecycleEvents.componentDebugStarted(name);
5985
- const timeoutMS = component.signalTimeoutMS;
5986
- let timeoutHandle;
5987
- const timeoutResult = { timedOut: true };
5988
- try {
5989
- const result = component.onDebug();
5990
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
5991
- const outcome = timeoutMS > 0 ? await Promise.race([
5992
- handlerPromise,
5993
- new Promise((resolve) => {
5994
- timeoutHandle = setTimeout(() => {
5995
- resolve(timeoutResult);
5996
- }, timeoutMS);
5997
- })
5998
- ]) : await handlerPromise;
5999
- if (outcome === timeoutResult) {
6000
- this.logger.entity(name).warn("Debug handler timed out", {
6001
- params: { timeoutMS }
6002
- });
6003
- Promise.resolve(handlerPromise).catch(() => {
6004
- });
6005
- results.push({
6006
- name,
6007
- called: true,
6008
- error: null,
6009
- timedOut: true,
6010
- code: "timeout"
6011
- });
6012
- } else {
6013
- this.lifecycleEvents.componentDebugCompleted(name);
6014
- results.push({
6015
- name,
6016
- called: true,
6017
- error: null,
6018
- timedOut: false,
6019
- code: "called"
6020
- });
6021
- }
6022
- } catch (error) {
6023
- const err = error instanceof Error ? error : new Error(String(error));
6024
- this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
6025
- params: { error: err }
6026
- });
6027
- this.lifecycleEvents.componentDebugFailed(name, err);
6028
- results.push({
6029
- name,
6030
- called: true,
6031
- error: err,
6032
- timedOut: false,
6033
- code: "error"
6034
- });
6035
- } finally {
6036
- if (timeoutHandle) {
6037
- clearTimeout(timeoutHandle);
6038
- }
6039
- }
6040
- }
6041
- const calledResults = results.filter((result) => result.called);
6042
- const hasError = calledResults.some((result) => result.error);
6043
- const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
6044
- const hasTimeout = calledResults.some((result) => result.timedOut);
6045
- const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
6046
- const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
6047
- return {
6304
+ return this.runSignalBroadcast({
6048
6305
  signal: "debug",
6049
- results,
6050
- timedOut: hasTimeout,
6051
- code
6052
- };
6306
+ pickHandler: (component) => component.onDebug?.bind(component),
6307
+ startupLog: "Debug during startup: only notifying already-started components",
6308
+ timeoutLog: "Debug handler timed out",
6309
+ errorLog: "Debug handler failed: {{error.message}}",
6310
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6311
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6312
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6313
+ });
6053
6314
  }
6054
6315
  };
6055
6316
 
@@ -6075,6 +6336,10 @@ var BaseComponent = class {
6075
6336
  name;
6076
6337
  /** Reference to component-scoped lifecycle (set by manager when registered) */
6077
6338
  lifecycle;
6339
+ /** @internal Set by LifecycleManager while the component is running. */
6340
+ _unexpectedStopHandler;
6341
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6342
+ _unexpectedStopGeneration = 0;
6078
6343
  /**
6079
6344
  * Create a new component
6080
6345
  *
@@ -6109,6 +6374,24 @@ var BaseComponent = class {
6109
6374
  // Default if undefined/null/non-finite
6110
6375
  );
6111
6376
  }
6377
+ /** @internal Called by LifecycleManager after a successful start. */
6378
+ _setUnexpectedStopHandler(handler) {
6379
+ this._unexpectedStopGeneration += 1;
6380
+ this._unexpectedStopHandler = handler;
6381
+ const generation = this._unexpectedStopGeneration;
6382
+ this.reportUnexpectedStop = (error) => {
6383
+ if (this._unexpectedStopGeneration !== generation) {
6384
+ return false;
6385
+ }
6386
+ return this._unexpectedStopHandler?.(error) ?? false;
6387
+ };
6388
+ }
6389
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6390
+ _clearUnexpectedStopHandler() {
6391
+ this._unexpectedStopGeneration += 1;
6392
+ this._unexpectedStopHandler = void 0;
6393
+ this.reportUnexpectedStop = () => false;
6394
+ }
6112
6395
  /**
6113
6396
  * Get component name
6114
6397
  */
@@ -6127,6 +6410,38 @@ var BaseComponent = class {
6127
6410
  isOptional() {
6128
6411
  return this.optional;
6129
6412
  }
6413
+ /**
6414
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6415
+ * successful start so captured references from older runs go stale.
6416
+ */
6417
+ reportUnexpectedStop = () => false;
6418
+ /**
6419
+ * Get this component's own status from the manager's perspective.
6420
+ *
6421
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6422
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6423
+ *
6424
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6425
+ */
6426
+ getSelfStatus() {
6427
+ return this.lifecycle?.getComponentStatus(this.name);
6428
+ }
6429
+ /**
6430
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6431
+ *
6432
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6433
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6434
+ * from a previous run from stopping a newer run of the same component instance.
6435
+ */
6436
+ getUnexpectedStopReporter() {
6437
+ const generation = this._unexpectedStopGeneration;
6438
+ return (error) => {
6439
+ if (this._unexpectedStopGeneration !== generation) {
6440
+ return false;
6441
+ }
6442
+ return this._unexpectedStopHandler?.(error) ?? false;
6443
+ };
6444
+ }
6130
6445
  };
6131
6446
  export {
6132
6447
  BaseComponent,