lifecycleion 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,26 @@ 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
+ var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE_EXTERNAL = "Component instance is already registered with another lifecycle manager.";
1431
+
1402
1432
  // src/lib/process-signal-manager.ts
1403
1433
  import { ulid } from "ulid";
1404
1434
  import readline from "readline";
@@ -1943,8 +1973,15 @@ var LifecycleManager = class extends EventEmitterProtected {
1943
1973
  componentTimestamps = /* @__PURE__ */ new Map();
1944
1974
  componentErrors = /* @__PURE__ */ new Map();
1945
1975
  componentStartAttemptTokens = /* @__PURE__ */ new Map();
1976
+ // Use per-stop ULIDs instead of incrementing counters because a stalled
1977
+ // component can be unregistered and replaced by a same-name instance before
1978
+ // the old floating stop promise settles.
1979
+ componentStopAttemptTokens = /* @__PURE__ */ new Map();
1980
+ pendingForceStopWaiters = /* @__PURE__ */ new Map();
1981
+ unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
1946
1982
  // State flags
1947
1983
  isStarting = false;
1984
+ autoAttachedSignalsDuringStartup = false;
1948
1985
  isStarted = false;
1949
1986
  isShuttingDown = false;
1950
1987
  // Unique token used to detect shutdowns that happened during async start().
@@ -2093,7 +2130,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2093
2130
  */
2094
2131
  async unregisterComponent(name, options) {
2095
2132
  if (this.isStarting || this.isShuttingDown) {
2096
- this.logger.entity(name).warn("Cannot unregister during bulk operation", {
2133
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
2097
2134
  params: {
2098
2135
  isStarting: this.isStarting,
2099
2136
  isShuttingDown: this.isShuttingDown
@@ -2102,7 +2139,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2102
2139
  return {
2103
2140
  success: false,
2104
2141
  componentName: name,
2105
- reason: "Cannot unregister during bulk operation",
2142
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
2106
2143
  code: "bulk_operation_in_progress",
2107
2144
  wasStopped: false,
2108
2145
  wasRegistered: this.hasComponent(name)
@@ -2110,11 +2147,11 @@ var LifecycleManager = class extends EventEmitterProtected {
2110
2147
  }
2111
2148
  const component = this.getComponent(name);
2112
2149
  if (!component) {
2113
- this.logger.entity(name).warn("Component not found");
2150
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
2114
2151
  return {
2115
2152
  success: false,
2116
2153
  componentName: name,
2117
- reason: "Component not found",
2154
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
2118
2155
  code: "component_not_found",
2119
2156
  wasStopped: false,
2120
2157
  wasRegistered: false
@@ -2127,7 +2164,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2127
2164
  return {
2128
2165
  success: false,
2129
2166
  componentName: name,
2130
- reason: "Component is stalled",
2167
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
2131
2168
  code: "stop_failed",
2132
2169
  stopFailureReason: "stalled",
2133
2170
  wasStopped: false,
@@ -2179,10 +2216,14 @@ var LifecycleManager = class extends EventEmitterProtected {
2179
2216
  wasStopped = true;
2180
2217
  }
2181
2218
  this.components = this.components.filter((c) => c.getName() !== name);
2219
+ component._clearUnexpectedStopHandler();
2220
+ component._markUnregistered();
2182
2221
  this.componentStates.delete(name);
2183
2222
  this.componentTimestamps.delete(name);
2184
2223
  this.componentErrors.delete(name);
2185
2224
  this.componentStartAttemptTokens.delete(name);
2225
+ this.componentStopAttemptTokens.delete(name);
2226
+ this.pendingForceStopWaiters.delete(name);
2186
2227
  this.stalledComponents.delete(name);
2187
2228
  this.runningComponents.delete(name);
2188
2229
  this.updateStartedFlag();
@@ -2529,7 +2570,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2529
2570
  startedComponents: [],
2530
2571
  failedOptionalComponents: [],
2531
2572
  skippedDueToDependency: [],
2532
- reason: "Shutdown in progress",
2573
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2533
2574
  code: "shutdown_in_progress",
2534
2575
  durationMS: Date.now() - startTime
2535
2576
  };
@@ -2589,6 +2630,8 @@ var LifecycleManager = class extends EventEmitterProtected {
2589
2630
  };
2590
2631
  }
2591
2632
  this.isStarting = true;
2633
+ this.autoAttachedSignalsDuringStartup = false;
2634
+ this.unexpectedStopsDuringStartup.clear();
2592
2635
  this.resetRepeatedShutdownRequestState();
2593
2636
  this.shutdownMethod = null;
2594
2637
  this.lastShutdownResult = null;
@@ -2721,13 +2764,49 @@ var LifecycleManager = class extends EventEmitterProtected {
2721
2764
  error: result.error,
2722
2765
  durationMS: Date.now() - startTime
2723
2766
  };
2767
+ } else if (result.code === "component_unexpected_stop") {
2768
+ this.unexpectedStopsDuringStartup.delete(name);
2769
+ const error = result.error || new Error(
2770
+ result.reason || `Component "${name}" stopped unexpectedly`
2771
+ );
2772
+ if (component.isOptional()) {
2773
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
2774
+ failedOptionalComponents.push({ name, error });
2775
+ }
2776
+ this.logger.entity(name).warn(
2777
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2778
+ {
2779
+ params: { error }
2780
+ }
2781
+ );
2782
+ } else {
2783
+ this.logger.entity(name).error(
2784
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
2785
+ {
2786
+ params: { error }
2787
+ }
2788
+ );
2789
+ await this.rollbackStartup(startedComponents);
2790
+ return {
2791
+ success: false,
2792
+ startedComponents: [],
2793
+ failedOptionalComponents,
2794
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2795
+ reason: error.message,
2796
+ code: "component_unexpected_stop",
2797
+ error,
2798
+ durationMS: Date.now() - startTime
2799
+ };
2800
+ }
2724
2801
  } else {
2725
2802
  if (component.isOptional()) {
2726
2803
  this.logger.entity(name).warn(
2727
2804
  "Optional component failed to start, continuing: {{error.message}}",
2728
2805
  {
2729
2806
  params: {
2730
- error: result.error || new Error(result.reason || "Unknown error")
2807
+ error: result.error || new Error(
2808
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2809
+ )
2731
2810
  }
2732
2811
  }
2733
2812
  );
@@ -2741,14 +2820,18 @@ var LifecycleManager = class extends EventEmitterProtected {
2741
2820
  }
2742
2821
  failedOptionalComponents.push({
2743
2822
  name,
2744
- error: result.error || new Error(result.reason || "Unknown error")
2823
+ error: result.error || new Error(
2824
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2825
+ )
2745
2826
  });
2746
2827
  } else {
2747
2828
  this.logger.entity(name).error(
2748
2829
  "Required component failed to start, rolling back: {{error.message}}",
2749
2830
  {
2750
2831
  params: {
2751
- error: result.error || new Error(result.reason || "Unknown error")
2832
+ error: result.error || new Error(
2833
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
2834
+ )
2752
2835
  }
2753
2836
  }
2754
2837
  );
@@ -2765,6 +2848,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2765
2848
  };
2766
2849
  }
2767
2850
  }
2851
+ const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
2852
+ startedComponents,
2853
+ failedOptionalComponents
2854
+ );
2855
+ startedComponents.splice(0, startedComponents.length);
2856
+ startedComponents.push(...unexpectedStopResult2.startedComponents);
2857
+ if (unexpectedStopResult2.requiredFailure) {
2858
+ await this.rollbackStartup(startedComponents);
2859
+ return {
2860
+ success: false,
2861
+ startedComponents: [],
2862
+ failedOptionalComponents,
2863
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2864
+ reason: unexpectedStopResult2.requiredFailure.error.message,
2865
+ code: "component_unexpected_stop",
2866
+ error: unexpectedStopResult2.requiredFailure.error,
2867
+ durationMS: Date.now() - startTime
2868
+ };
2869
+ }
2768
2870
  }
2769
2871
  if (hasTimedOut) {
2770
2872
  const durationMS2 = Date.now() - startTime;
@@ -2788,6 +2890,25 @@ var LifecycleManager = class extends EventEmitterProtected {
2788
2890
  code: "startup_timeout"
2789
2891
  };
2790
2892
  }
2893
+ const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
2894
+ startedComponents,
2895
+ failedOptionalComponents
2896
+ );
2897
+ startedComponents.splice(0, startedComponents.length);
2898
+ startedComponents.push(...unexpectedStopResult.startedComponents);
2899
+ if (unexpectedStopResult.requiredFailure) {
2900
+ await this.rollbackStartup(startedComponents);
2901
+ return {
2902
+ success: false,
2903
+ startedComponents: [],
2904
+ failedOptionalComponents,
2905
+ skippedDueToDependency: Array.from(skippedDueToDependency),
2906
+ reason: unexpectedStopResult.requiredFailure.error.message,
2907
+ code: "component_unexpected_stop",
2908
+ error: unexpectedStopResult.requiredFailure.error,
2909
+ durationMS: Date.now() - startTime
2910
+ };
2911
+ }
2791
2912
  this.updateStartedFlag();
2792
2913
  const skippedComponentsArray = [
2793
2914
  ...Array.from(skippedDueToDependency),
@@ -2819,10 +2940,12 @@ var LifecycleManager = class extends EventEmitterProtected {
2819
2940
  if (timeoutHandle) {
2820
2941
  clearTimeout(timeoutHandle);
2821
2942
  }
2822
- if (didAutoAttachSignalsForBulkStartup) {
2943
+ this.isStarting = false;
2944
+ if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
2823
2945
  this.autoDetachSignalsIfIdle("failed bulk startup");
2824
2946
  }
2825
- this.isStarting = false;
2947
+ this.autoAttachedSignalsDuringStartup = false;
2948
+ this.unexpectedStopsDuringStartup.clear();
2826
2949
  }
2827
2950
  }
2828
2951
  /**
@@ -2886,7 +3009,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2886
3009
  return {
2887
3010
  success: false,
2888
3011
  componentName: name,
2889
- reason: "Bulk startup in progress",
3012
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
2890
3013
  code: "startup_in_progress"
2891
3014
  };
2892
3015
  }
@@ -2897,7 +3020,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2897
3020
  return {
2898
3021
  success: false,
2899
3022
  componentName: name,
2900
- reason: "Shutdown in progress",
3023
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2901
3024
  code: "shutdown_in_progress"
2902
3025
  };
2903
3026
  }
@@ -2931,7 +3054,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2931
3054
  return {
2932
3055
  success: false,
2933
3056
  componentName: name,
2934
- reason: this.isStarting ? "Bulk startup in progress" : "Shutdown in progress",
3057
+ reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
2935
3058
  code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
2936
3059
  };
2937
3060
  }
@@ -3113,7 +3236,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3113
3236
  if (this.isShuttingDown) {
3114
3237
  if (isFirstExit && this.pendingLoggerExitResolve === null) {
3115
3238
  this.logger.debug(
3116
- "Logger exit called during shutdown, waiting...",
3239
+ LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
3117
3240
  {
3118
3241
  params: { exitCode }
3119
3242
  }
@@ -3122,7 +3245,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3122
3245
  this.pendingLoggerExitResolve = resolve;
3123
3246
  });
3124
3247
  }
3125
- this.logger.debug("Logger exit called during shutdown, waiting...", {
3248
+ this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
3126
3249
  params: { exitCode }
3127
3250
  });
3128
3251
  return { action: "wait" };
@@ -3212,7 +3335,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3212
3335
  return {
3213
3336
  name,
3214
3337
  healthy: false,
3215
- message: "Component not found",
3338
+ message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
3216
3339
  checkedAt: startTime,
3217
3340
  durationMS: 0,
3218
3341
  error: null,
@@ -3225,7 +3348,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3225
3348
  return {
3226
3349
  name,
3227
3350
  healthy: false,
3228
- message: isStalled ? "Component is stalled" : "Component not running",
3351
+ message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
3229
3352
  checkedAt: startTime,
3230
3353
  durationMS: Date.now() - startTime,
3231
3354
  error: null,
@@ -3441,7 +3564,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3441
3564
  result = component.onMessage(payload, from);
3442
3565
  } catch (error) {
3443
3566
  const err = error instanceof Error ? error : new Error(String(error));
3444
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3567
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3445
3568
  params: { error: err, from }
3446
3569
  });
3447
3570
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3501,7 +3624,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3501
3624
  };
3502
3625
  } catch (error) {
3503
3626
  const err = error instanceof Error ? error : new Error(String(error));
3504
- this.logger.entity(componentName).error("Message handler failed: {{error.message}}", {
3627
+ this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
3505
3628
  params: { error: err, from, timeoutMS }
3506
3629
  });
3507
3630
  this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
@@ -3774,7 +3897,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3774
3897
  this.lifecycleEvents.componentRegistrationRejected({
3775
3898
  name: componentName,
3776
3899
  reason: "shutdown_in_progress",
3777
- message: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3900
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3778
3901
  registrationIndexBefore,
3779
3902
  registrationIndexAfter: registrationIndexBefore,
3780
3903
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3786,7 +3909,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3786
3909
  targetComponentName,
3787
3910
  registrationIndexBefore,
3788
3911
  code: "shutdown_in_progress",
3789
- reason: "Cannot register component while shutdown is in progress (isShuttingDown=true).",
3912
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
3790
3913
  targetFound: void 0
3791
3914
  });
3792
3915
  }
@@ -3797,7 +3920,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3797
3920
  this.lifecycleEvents.componentRegistrationRejected({
3798
3921
  name: componentName,
3799
3922
  reason: "startup_in_progress",
3800
- message: "Cannot register component during startup when it is a required dependency for other components.",
3923
+ message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3801
3924
  registrationIndexBefore,
3802
3925
  registrationIndexAfter: registrationIndexBefore,
3803
3926
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3809,16 +3932,20 @@ var LifecycleManager = class extends EventEmitterProtected {
3809
3932
  targetComponentName,
3810
3933
  registrationIndexBefore,
3811
3934
  code: "startup_in_progress",
3812
- reason: "Cannot register component during startup when it is a required dependency for other components.",
3935
+ reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
3813
3936
  targetFound: void 0
3814
3937
  });
3815
3938
  }
3816
- if (this.hasComponentInstance(component)) {
3817
- this.logger.entity(componentName).warn("Component instance already registered");
3939
+ if (component._isRegisteredWithManager()) {
3940
+ const isRegisteredHere = this.hasComponentInstance(component);
3941
+ const message = isRegisteredHere ? LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE : LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE_EXTERNAL;
3942
+ this.logger.entity(componentName).warn(
3943
+ isRegisteredHere ? "Component instance already registered" : "Component instance already registered with another lifecycle manager"
3944
+ );
3818
3945
  this.lifecycleEvents.componentRegistrationRejected({
3819
3946
  name: componentName,
3820
3947
  reason: "duplicate_instance",
3821
- message: "Component instance is already registered.",
3948
+ message,
3822
3949
  registrationIndexBefore,
3823
3950
  registrationIndexAfter: registrationIndexBefore,
3824
3951
  requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
@@ -3830,7 +3957,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3830
3957
  targetComponentName,
3831
3958
  registrationIndexBefore,
3832
3959
  code: "duplicate_instance",
3833
- reason: "Component instance is already registered.",
3960
+ reason: message,
3834
3961
  targetFound: void 0
3835
3962
  });
3836
3963
  }
@@ -3943,6 +4070,7 @@ var LifecycleManager = class extends EventEmitterProtected {
3943
4070
  getValueInternal: (compName, key, from) => this.getValueInternal(compName, key, from)
3944
4071
  };
3945
4072
  component.lifecycle = new ComponentLifecycle(this, componentName, internalCallbacks);
4073
+ component._markRegistered();
3946
4074
  this.componentStates.set(componentName, "registered");
3947
4075
  this.componentTimestamps.set(componentName, {
3948
4076
  startedAt: null,
@@ -4144,8 +4272,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4144
4272
  const runningComponentsToStop = shutdownOrder.filter(
4145
4273
  (name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
4146
4274
  );
4147
- const stoppedComponents = [];
4148
- const stalledComponents = [];
4275
+ const stoppedComponents = /* @__PURE__ */ new Set();
4149
4276
  let hasTimedOut = false;
4150
4277
  let timeoutHandle;
4151
4278
  try {
@@ -4176,34 +4303,37 @@ var LifecycleManager = class extends EventEmitterProtected {
4176
4303
  this.logger.entity(name).info("Stopping component");
4177
4304
  const isRunning = this.isComponentRunning(name);
4178
4305
  const isStalled = stalledComponentNames.has(name);
4306
+ const currentState = this.componentStates.get(name);
4307
+ if (currentState === "stopped") {
4308
+ stoppedComponents.add(name);
4309
+ continue;
4310
+ }
4179
4311
  const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
4180
4312
  success: false,
4181
4313
  componentName: name,
4182
- reason: "Component is stalled",
4314
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4183
4315
  code: "component_stalled",
4184
4316
  status: this.getComponentStatus(name)
4185
4317
  } : {
4186
4318
  success: false,
4187
4319
  componentName: name,
4188
- reason: "Component not running",
4320
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4189
4321
  code: "component_not_running",
4190
4322
  status: this.getComponentStatus(name)
4191
4323
  };
4192
4324
  if (result2.success) {
4193
- stoppedComponents.push(name);
4325
+ stoppedComponents.add(name);
4194
4326
  } else {
4195
4327
  this.logger.entity(name).error(
4196
4328
  "Component failed to stop, continuing with others: {{error.message}}",
4197
4329
  {
4198
4330
  params: {
4199
- error: result2.error || new Error(result2.reason || "Unknown error")
4331
+ error: result2.error || new Error(
4332
+ result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
4333
+ )
4200
4334
  }
4201
4335
  }
4202
4336
  );
4203
- const stallInfo = this.stalledComponents.get(name);
4204
- if (stallInfo) {
4205
- stalledComponents.push(stallInfo);
4206
- }
4207
4337
  if (shouldHaltOnStall) {
4208
4338
  this.logger.warn(
4209
4339
  "Halting shutdown after stall (haltOnStall=true)",
@@ -4219,21 +4349,32 @@ var LifecycleManager = class extends EventEmitterProtected {
4219
4349
  } else {
4220
4350
  await shutdownOperation();
4221
4351
  }
4352
+ const finalStalledNames = /* @__PURE__ */ new Set();
4353
+ for (const name of runningComponentsToStop) {
4354
+ if (this.stalledComponents.has(name)) {
4355
+ finalStalledNames.add(name);
4356
+ }
4357
+ }
4222
4358
  if (!shouldRetryStalled) {
4223
4359
  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);
4360
+ if (this.stalledComponents.has(name)) {
4361
+ finalStalledNames.add(name);
4227
4362
  }
4228
4363
  }
4229
4364
  }
4365
+ for (const name of runningComponentsToStop) {
4366
+ if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
4367
+ stoppedComponents.add(name);
4368
+ }
4369
+ }
4370
+ const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
4230
4371
  const durationMS = Date.now() - startTime;
4231
4372
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4232
4373
  this.logger[isSuccess ? "success" : "warn"](
4233
4374
  isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4234
4375
  {
4235
4376
  params: {
4236
- stopped: stoppedComponents.length,
4377
+ stopped: stoppedComponents.size,
4237
4378
  stalled: stalledComponents.length,
4238
4379
  durationMS
4239
4380
  }
@@ -4241,7 +4382,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4241
4382
  );
4242
4383
  const result = {
4243
4384
  success: isSuccess,
4244
- stoppedComponents,
4385
+ stoppedComponents: Array.from(stoppedComponents),
4245
4386
  stalledComponents,
4246
4387
  durationMS,
4247
4388
  timedOut: hasTimedOut || void 0,
@@ -4292,7 +4433,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4292
4433
  return {
4293
4434
  success: false,
4294
4435
  componentName: name,
4295
- reason: "Component not found",
4436
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4296
4437
  code: "component_not_found"
4297
4438
  };
4298
4439
  }
@@ -4303,12 +4444,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4303
4444
  return {
4304
4445
  success: false,
4305
4446
  componentName: name,
4306
- reason: "Component not running",
4447
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4307
4448
  code: "component_not_running",
4308
4449
  status: this.getComponentStatus(name)
4309
4450
  };
4310
4451
  }
4311
4452
  this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
4453
+ if (component.onShutdownForce) {
4454
+ this.issueStopAttemptToken(name);
4455
+ }
4312
4456
  return this.shutdownComponentForce(name, component, {
4313
4457
  gracefulPhaseRan: false,
4314
4458
  gracefulTimedOut: false,
@@ -4328,7 +4472,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4328
4472
  return {
4329
4473
  success: false,
4330
4474
  componentName: name,
4331
- reason: "Shutdown in progress",
4475
+ reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
4332
4476
  code: "shutdown_in_progress"
4333
4477
  };
4334
4478
  }
@@ -4340,7 +4484,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4340
4484
  return {
4341
4485
  success: false,
4342
4486
  componentName: name,
4343
- reason: "Bulk startup in progress",
4487
+ reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
4344
4488
  code: "startup_in_progress"
4345
4489
  };
4346
4490
  }
@@ -4350,7 +4494,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4350
4494
  return {
4351
4495
  success: false,
4352
4496
  componentName: name,
4353
- reason: "Component not found",
4497
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4354
4498
  code: "component_not_found"
4355
4499
  };
4356
4500
  }
@@ -4359,7 +4503,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4359
4503
  return {
4360
4504
  success: false,
4361
4505
  componentName: name,
4362
- reason: "Component is stalled",
4506
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4363
4507
  code: "component_stalled",
4364
4508
  status: this.getComponentStatus(name)
4365
4509
  };
@@ -4423,6 +4567,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4423
4567
  const timeoutMS = component.startupTimeoutMS;
4424
4568
  const startAttemptToken = ulid2();
4425
4569
  this.componentStartAttemptTokens.set(name, startAttemptToken);
4570
+ component._setUnexpectedStopHandler(
4571
+ (error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
4572
+ );
4426
4573
  const shutdownTokenAtStart = this.shutdownToken;
4427
4574
  const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
4428
4575
  let timeoutHandle;
@@ -4465,6 +4612,18 @@ var LifecycleManager = class extends EventEmitterProtected {
4465
4612
  } else {
4466
4613
  await startPromise;
4467
4614
  }
4615
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
4616
+ component._clearUnexpectedStopHandler();
4617
+ const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
4618
+ return {
4619
+ success: false,
4620
+ componentName: name,
4621
+ reason: error.message,
4622
+ code: "component_unexpected_stop",
4623
+ error,
4624
+ status: this.getComponentStatus(name)
4625
+ };
4626
+ }
4468
4627
  if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
4469
4628
  this.componentStates.set(name, "running");
4470
4629
  this.runningComponents.add(name);
@@ -4492,6 +4651,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4492
4651
  this.componentStates.set(name, "running");
4493
4652
  this.runningComponents.add(name);
4494
4653
  this.stalledComponents.delete(name);
4654
+ if (shouldForceStalled) {
4655
+ this.issueStopAttemptToken(name);
4656
+ }
4495
4657
  this.updateStartedFlag();
4496
4658
  if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
4497
4659
  this.autoAttachSignals("first component start");
@@ -4511,9 +4673,24 @@ var LifecycleManager = class extends EventEmitterProtected {
4511
4673
  status: this.getComponentStatus(name)
4512
4674
  };
4513
4675
  } catch (error) {
4676
+ component._clearUnexpectedStopHandler();
4514
4677
  const err = error instanceof Error ? error : new Error(String(error));
4678
+ const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
4679
+ const unexpectedStopError = this.componentErrors.get(name);
4680
+ if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
4681
+ return {
4682
+ success: false,
4683
+ componentName: name,
4684
+ reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
4685
+ code: "component_unexpected_stop",
4686
+ error: unexpectedStopError || new Error(
4687
+ `Component "${name}" stopped unexpectedly during startup`
4688
+ ),
4689
+ status: this.getComponentStatus(name)
4690
+ };
4691
+ }
4515
4692
  this.componentErrors.set(name, err);
4516
- if (err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name) {
4693
+ if (isStartupTimeout) {
4517
4694
  this.componentStates.set(name, "starting-timed-out");
4518
4695
  this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
4519
4696
  params: { error: err }
@@ -4558,7 +4735,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4558
4735
  return {
4559
4736
  success: false,
4560
4737
  componentName: name,
4561
- reason: "Component not found",
4738
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
4562
4739
  code: "component_not_found"
4563
4740
  };
4564
4741
  }
@@ -4566,7 +4743,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4566
4743
  return {
4567
4744
  success: false,
4568
4745
  componentName: name,
4569
- reason: "Component is stalled",
4746
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
4570
4747
  code: "component_stalled",
4571
4748
  status: this.getComponentStatus(name)
4572
4749
  };
@@ -4575,7 +4752,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4575
4752
  return {
4576
4753
  success: false,
4577
4754
  componentName: name,
4578
- reason: "Component not running",
4755
+ reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
4579
4756
  code: "component_not_running",
4580
4757
  status: this.getComponentStatus(name)
4581
4758
  };
@@ -4591,6 +4768,10 @@ var LifecycleManager = class extends EventEmitterProtected {
4591
4768
  };
4592
4769
  }
4593
4770
  if (options?.forceImmediate) {
4771
+ if (component.onShutdownForce) {
4772
+ this.issueStopAttemptToken(name);
4773
+ }
4774
+ component._clearUnexpectedStopHandler();
4594
4775
  return this.shutdownComponentForce(name, component, {
4595
4776
  gracefulPhaseRan: false,
4596
4777
  gracefulTimedOut: false,
@@ -4727,9 +4908,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4727
4908
  * Calls stop() with timeout
4728
4909
  */
4729
4910
  async shutdownComponentGraceful(name, component, options) {
4911
+ component._clearUnexpectedStopHandler();
4730
4912
  this.componentStates.set(name, "stopping");
4731
4913
  this.logger.entity(name).info("Graceful shutdown started");
4732
4914
  this.lifecycleEvents.componentStopping(name);
4915
+ const stopAttemptToken = this.issueStopAttemptToken(name);
4733
4916
  const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
4734
4917
  let timeoutHandle;
4735
4918
  try {
@@ -4750,7 +4933,16 @@ var LifecycleManager = class extends EventEmitterProtected {
4750
4933
  );
4751
4934
  }
4752
4935
  }
4753
- Promise.resolve(stopPromise).catch(() => {
4936
+ Promise.resolve(stopPromise).then(
4937
+ () => this.handleLateStopResolution(
4938
+ name,
4939
+ stopAttemptToken,
4940
+ "graceful"
4941
+ ),
4942
+ () => {
4943
+ }
4944
+ // Intentionally ignore errors after timeout
4945
+ ).catch(() => {
4754
4946
  });
4755
4947
  reject(
4756
4948
  new ComponentStopTimeoutError({
@@ -4769,9 +4961,7 @@ var LifecycleManager = class extends EventEmitterProtected {
4769
4961
  this.stalledComponents.delete(name);
4770
4962
  this.updateStartedFlag();
4771
4963
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4772
- this.logger.info(
4773
- "Auto-detaching process signals on last component stop"
4774
- );
4964
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4775
4965
  this.detachSignals();
4776
4966
  }
4777
4967
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4794,15 +4984,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4794
4984
  const err = error instanceof Error ? error : new Error(String(error));
4795
4985
  this.componentErrors.set(name, err);
4796
4986
  if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
4797
- this.logger.entity(name).warn("Graceful shutdown timed out");
4987
+ this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
4798
4988
  this.lifecycleEvents.componentStopTimeout(name, err, {
4799
4989
  timeoutMS,
4800
- reason: "Graceful shutdown timed out"
4990
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
4801
4991
  });
4802
4992
  return {
4803
4993
  success: false,
4804
4994
  componentName: name,
4805
- reason: "Graceful shutdown timed out",
4995
+ reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
4806
4996
  code: "component_shutdown_timeout",
4807
4997
  error: err,
4808
4998
  status: this.getComponentStatus(name)
@@ -4845,8 +5035,6 @@ var LifecycleManager = class extends EventEmitterProtected {
4845
5035
  gracefulTimedOut: context.gracefulTimedOut
4846
5036
  }
4847
5037
  });
4848
- const timeoutMS = component.shutdownForceTimeoutMS;
4849
- let timeoutHandle;
4850
5038
  if (!component.onShutdownForce) {
4851
5039
  const stallInfo = {
4852
5040
  name,
@@ -4880,6 +5068,9 @@ var LifecycleManager = class extends EventEmitterProtected {
4880
5068
  status: this.getComponentStatus(name)
4881
5069
  };
4882
5070
  }
5071
+ const timeoutMS = component.shutdownForceTimeoutMS;
5072
+ const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
5073
+ let timeoutHandle;
4883
5074
  try {
4884
5075
  const forcePromise = component.onShutdownForce();
4885
5076
  if (timeoutMS > 0) {
@@ -4898,23 +5089,44 @@ var LifecycleManager = class extends EventEmitterProtected {
4898
5089
  );
4899
5090
  }
4900
5091
  }
4901
- Promise.resolve(forcePromise).catch(() => {
5092
+ const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? ulid2();
5093
+ Promise.resolve(forcePromise).then(
5094
+ () => this.handleLateStopResolution(
5095
+ name,
5096
+ forceAttemptToken,
5097
+ "force"
5098
+ ),
5099
+ () => {
5100
+ }
5101
+ // Intentionally ignore errors after timeout
5102
+ ).catch(() => {
4902
5103
  });
4903
- reject(new Error("Force shutdown timed out"));
5104
+ reject(
5105
+ new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
5106
+ );
4904
5107
  }, timeoutMS);
4905
5108
  });
4906
- await Promise.race([forcePromise, timeoutPromise]);
5109
+ await Promise.race([
5110
+ forcePromise,
5111
+ timeoutPromise,
5112
+ stoppedDuringForcePromise
5113
+ ]);
4907
5114
  } else {
4908
- await forcePromise;
5115
+ await Promise.race([forcePromise, stoppedDuringForcePromise]);
5116
+ }
5117
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5118
+ return {
5119
+ success: true,
5120
+ componentName: name,
5121
+ status: this.getComponentStatus(name)
5122
+ };
4909
5123
  }
4910
5124
  this.componentStates.set(name, "stopped");
4911
5125
  this.runningComponents.delete(name);
4912
5126
  this.stalledComponents.delete(name);
4913
5127
  this.updateStartedFlag();
4914
5128
  if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
4915
- this.logger.info(
4916
- "Auto-detaching process signals on last component stop"
4917
- );
5129
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
4918
5130
  this.detachSignals();
4919
5131
  }
4920
5132
  const timestamps = this.componentTimestamps.get(name) ?? {
@@ -4935,8 +5147,15 @@ var LifecycleManager = class extends EventEmitterProtected {
4935
5147
  status: this.getComponentStatus(name)
4936
5148
  };
4937
5149
  } catch (error) {
5150
+ if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
5151
+ return {
5152
+ success: true,
5153
+ componentName: name,
5154
+ status: this.getComponentStatus(name)
5155
+ };
5156
+ }
4938
5157
  const err = error instanceof Error ? error : new Error(String(error));
4939
- const isTimeout = err.message === "Force shutdown timed out";
5158
+ const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
4940
5159
  const stallInfo = {
4941
5160
  name,
4942
5161
  phase: "force",
@@ -4967,12 +5186,13 @@ var LifecycleManager = class extends EventEmitterProtected {
4967
5186
  return {
4968
5187
  success: false,
4969
5188
  componentName: name,
4970
- reason: isTimeout ? "Force shutdown timed out" : err.message,
5189
+ reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
4971
5190
  code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
4972
5191
  error: err,
4973
5192
  status: this.getComponentStatus(name)
4974
5193
  };
4975
5194
  } finally {
5195
+ cleanupForceWaiter();
4976
5196
  if (timeoutHandle) {
4977
5197
  clearTimeout(timeoutHandle);
4978
5198
  }
@@ -5049,7 +5269,9 @@ var LifecycleManager = class extends EventEmitterProtected {
5049
5269
  "Failed to stop component during rollback, continuing: {{error.message}}",
5050
5270
  {
5051
5271
  params: {
5052
- error: result.error || new Error(result.reason || "Unknown error")
5272
+ error: result.error || new Error(
5273
+ result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
5274
+ )
5053
5275
  }
5054
5276
  }
5055
5277
  );
@@ -5063,10 +5285,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5063
5285
  }
5064
5286
  this.logger.info(`Auto-attaching process signals on ${trigger}`);
5065
5287
  this.attachSignals();
5288
+ if (this.isStarting) {
5289
+ this.autoAttachedSignalsDuringStartup = true;
5290
+ }
5066
5291
  return true;
5067
5292
  }
5068
5293
  autoDetachSignalsIfIdle(trigger) {
5069
- if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5294
+ if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
5070
5295
  return;
5071
5296
  }
5072
5297
  this.logger.info(`Auto-detaching process signals after ${trigger}`);
@@ -5108,6 +5333,210 @@ var LifecycleManager = class extends EventEmitterProtected {
5108
5333
  }).catch(() => {
5109
5334
  });
5110
5335
  }
5336
+ consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
5337
+ if (this.unexpectedStopsDuringStartup.size === 0) {
5338
+ return { startedComponents: [...startedComponents] };
5339
+ }
5340
+ const remainingStartedComponents = [];
5341
+ let requiredFailure;
5342
+ for (const name of startedComponents) {
5343
+ const startupStopError = this.unexpectedStopsDuringStartup.get(name);
5344
+ if (startupStopError === void 0) {
5345
+ remainingStartedComponents.push(name);
5346
+ continue;
5347
+ }
5348
+ this.unexpectedStopsDuringStartup.delete(name);
5349
+ const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
5350
+ const component = this.getComponent(name);
5351
+ if (component?.isOptional()) {
5352
+ if (!failedOptionalComponents.some((entry) => entry.name === name)) {
5353
+ failedOptionalComponents.push({ name, error });
5354
+ }
5355
+ this.logger.entity(name).warn(
5356
+ LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5357
+ {
5358
+ params: { error }
5359
+ }
5360
+ );
5361
+ continue;
5362
+ }
5363
+ this.logger.entity(name).error(
5364
+ LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
5365
+ {
5366
+ params: { error }
5367
+ }
5368
+ );
5369
+ requiredFailure ??= { name, error };
5370
+ }
5371
+ return {
5372
+ startedComponents: remainingStartedComponents,
5373
+ requiredFailure
5374
+ };
5375
+ }
5376
+ /**
5377
+ * Issues and returns a unique stop attempt token for a component.
5378
+ *
5379
+ * Each stop attempt (graceful or force-retry) gets a unique token.
5380
+ * The late-resolution handler captures this token in its closure so it can
5381
+ * skip any stall entries that were created by a *later* stop attempt — e.g. a
5382
+ * force-retry that also timed out after the original graceful promise floated
5383
+ * in the background.
5384
+ */
5385
+ issueStopAttemptToken(name) {
5386
+ const next = ulid2();
5387
+ this.componentStopAttemptTokens.set(name, next);
5388
+ return next;
5389
+ }
5390
+ createPendingForceStopWaiter(name) {
5391
+ let isResolved = false;
5392
+ let waiters = this.pendingForceStopWaiters.get(name);
5393
+ if (!waiters) {
5394
+ waiters = /* @__PURE__ */ new Set();
5395
+ this.pendingForceStopWaiters.set(name, waiters);
5396
+ }
5397
+ let resolveWaiter;
5398
+ const promise = new Promise((resolve) => {
5399
+ resolveWaiter = () => {
5400
+ if (isResolved) {
5401
+ return;
5402
+ }
5403
+ isResolved = true;
5404
+ resolve();
5405
+ };
5406
+ });
5407
+ waiters.add(resolveWaiter);
5408
+ return {
5409
+ promise,
5410
+ cleanup: () => {
5411
+ const pending = this.pendingForceStopWaiters.get(name);
5412
+ if (!pending) {
5413
+ return;
5414
+ }
5415
+ pending.delete(resolveWaiter);
5416
+ if (pending.size === 0) {
5417
+ this.pendingForceStopWaiters.delete(name);
5418
+ }
5419
+ }
5420
+ };
5421
+ }
5422
+ resolvePendingForceStopWaiters(name) {
5423
+ const waiters = this.pendingForceStopWaiters.get(name);
5424
+ if (!waiters || waiters.size === 0) {
5425
+ return;
5426
+ }
5427
+ this.pendingForceStopWaiters.delete(name);
5428
+ for (const resolve of waiters) {
5429
+ resolve();
5430
+ }
5431
+ }
5432
+ /**
5433
+ * Called when a stop promise eventually resolves after its timeout path already fired.
5434
+ *
5435
+ * Usually this means a previously stalled component's original stop() or
5436
+ * onShutdownForce() promise finally resolved, so the manager can clear the
5437
+ * stall and transition the component to stopped without a manual retry.
5438
+ *
5439
+ * There is one extra overlap case for graceful stop(): stop() can resolve
5440
+ * after the graceful timeout but before onShutdownForce() itself times out.
5441
+ * In that window no stall entry exists yet, but the component still finished
5442
+ * stopping cleanly, so we finalize it here and let the later force-timeout
5443
+ * path observe the already-stopped state and no-op. This overlap fix is
5444
+ * scoped to the same stop token and will not cross a later retry attempt.
5445
+ *
5446
+ * Two guards prevent stale floating promises from incorrectly clearing state:
5447
+ *
5448
+ * 1. token guard — if a newer stop attempt (e.g. a retryStalled
5449
+ * force-retry) has started since this promise was launched, its token
5450
+ * won't match and we bail out immediately.
5451
+ *
5452
+ * 2. state/stall guard — if the component was unregistered, restarted, or
5453
+ * already cleared by another path, there will be neither a matching stall
5454
+ * entry nor the force-phase overlap state, so we bail out.
5455
+ */
5456
+ handleLateStopResolution(name, token, source) {
5457
+ if (this.componentStopAttemptTokens.get(name) !== token) {
5458
+ return;
5459
+ }
5460
+ const currentState = this.componentStates.get(name);
5461
+ const stallInfo = this.stalledComponents.get(name);
5462
+ const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
5463
+ if (stallInfo && currentState !== "stalled") {
5464
+ this.stalledComponents.delete(name);
5465
+ return;
5466
+ }
5467
+ if (!stallInfo && !isCompletedDuringForcePhase) {
5468
+ return;
5469
+ }
5470
+ const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
5471
+ if (stallInfo) {
5472
+ this.stalledComponents.delete(name);
5473
+ }
5474
+ this.componentStates.set(name, "stopped");
5475
+ this.runningComponents.delete(name);
5476
+ this.componentErrors.set(name, null);
5477
+ this.updateStartedFlag();
5478
+ this.resolvePendingForceStopWaiters(name);
5479
+ if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
5480
+ this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
5481
+ this.detachSignals();
5482
+ }
5483
+ const timestamps = this.componentTimestamps.get(name) ?? {
5484
+ startedAt: null,
5485
+ stoppedAt: null
5486
+ };
5487
+ timestamps.stoppedAt = Date.now();
5488
+ this.componentTimestamps.set(name, timestamps);
5489
+ this.logger.entity(name).info(
5490
+ stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
5491
+ stalledDurationMS ? { params: { stalledDurationMS } } : void 0
5492
+ );
5493
+ if (source === "force") {
5494
+ this.lifecycleEvents.componentShutdownForceCompleted(name);
5495
+ }
5496
+ if (stallInfo && stalledDurationMS !== void 0) {
5497
+ this.lifecycleEvents.componentStalledResolved(
5498
+ name,
5499
+ stallInfo,
5500
+ stalledDurationMS
5501
+ );
5502
+ }
5503
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5504
+ }
5505
+ handleComponentUnexpectedStop(name, startAttemptToken, error) {
5506
+ const currentState = this.componentStates.get(name);
5507
+ if (
5508
+ // Startup-time self-stops are valid too: start() may still be awaiting
5509
+ // some async work while an internal listener has already observed that
5510
+ // the component died and reported it.
5511
+ currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
5512
+ ) {
5513
+ return false;
5514
+ }
5515
+ this.runningComponents.delete(name);
5516
+ this.componentStates.set(name, "stopped");
5517
+ this.componentErrors.set(name, error ?? null);
5518
+ if (this.isStarting) {
5519
+ this.unexpectedStopsDuringStartup.set(name, error ?? null);
5520
+ }
5521
+ this.updateStartedFlag();
5522
+ if (this.detachSignalsOnStop && !this.isStarting && 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).warn(
5533
+ error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
5534
+ { params: { error } }
5535
+ );
5536
+ this.lifecycleEvents.componentUnexpectedStop(name, error);
5537
+ this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
5538
+ return true;
5539
+ }
5111
5540
  /**
5112
5541
  * Safe emit wrapper - prevents event handler errors from breaking lifecycle
5113
5542
  */
@@ -5679,105 +6108,87 @@ var LifecycleManager = class extends EventEmitterProtected {
5679
6108
  }
5680
6109
  }
5681
6110
  /**
5682
- * Handle reload request - calls custom callback or broadcasts to components.
6111
+ * Shared dispatch path for reload/info/debug requests. Logs the dispatch,
6112
+ * emits the signal event, then either invokes the user-supplied callback
6113
+ * (passing the broadcast function so the user controls when/whether to
6114
+ * broadcast) or broadcasts directly when no callback is configured.
5683
6115
  *
5684
6116
  * 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
- *
6117
+ * but not awaited Node.js signal handlers cannot return values, so results
6118
+ * are not accessible. Components are still notified and the work completes.
5688
6119
  * When called from manual triggers (source='trigger'), the Promise is awaited
5689
6120
  * and results are returned for programmatic use.
5690
- *
5691
- * @param source - Whether triggered from signal manager or manual trigger
5692
6121
  */
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);
6122
+ async handleSignalRequest(descriptor, source) {
6123
+ this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
6124
+ descriptor.emitSignal();
6125
+ if (descriptor.customCallback) {
6126
+ const result = descriptor.customCallback(descriptor.broadcast);
5699
6127
  if (isPromise(result)) {
5700
6128
  await result;
5701
6129
  }
5702
6130
  return {
5703
- signal: "reload",
6131
+ signal: descriptor.signal,
5704
6132
  results: [],
5705
6133
  timedOut: false,
5706
6134
  code: "ok"
5707
6135
  };
5708
6136
  }
5709
- return this.broadcastReload();
6137
+ return descriptor.broadcast();
6138
+ }
6139
+ async handleReloadRequest(source = "trigger") {
6140
+ return this.handleSignalRequest(
6141
+ {
6142
+ signal: "reload",
6143
+ dispatchedLogLabel: "Reload dispatched",
6144
+ emitSignal: () => this.lifecycleEvents.signalReload(),
6145
+ customCallback: this.onReloadRequested,
6146
+ broadcast: () => this.broadcastReload()
6147
+ },
6148
+ source
6149
+ );
5710
6150
  }
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
6151
  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 {
6152
+ return this.handleSignalRequest(
6153
+ {
5729
6154
  signal: "info",
5730
- results: [],
5731
- timedOut: false,
5732
- code: "ok"
5733
- };
5734
- }
5735
- return this.broadcastInfo();
6155
+ dispatchedLogLabel: "Info dispatched",
6156
+ emitSignal: () => this.lifecycleEvents.signalInfo(),
6157
+ customCallback: this.onInfoRequested,
6158
+ broadcast: () => this.broadcastInfo()
6159
+ },
6160
+ source
6161
+ );
5736
6162
  }
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
6163
  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 {
6164
+ return this.handleSignalRequest(
6165
+ {
5755
6166
  signal: "debug",
5756
- results: [],
5757
- timedOut: false,
5758
- code: "ok"
5759
- };
5760
- }
5761
- return this.broadcastDebug();
6167
+ dispatchedLogLabel: "Debug dispatched",
6168
+ emitSignal: () => this.lifecycleEvents.signalDebug(),
6169
+ customCallback: this.onDebugRequested,
6170
+ broadcast: () => this.broadcastDebug()
6171
+ },
6172
+ source
6173
+ );
5762
6174
  }
5763
6175
  /**
5764
- * Broadcast reload signal to all running components.
5765
- * Calls onReload() on components that implement it.
5766
- * Continues on errors - collects all results.
6176
+ * Shared signal broadcast pipeline used by reload/info/debug.
6177
+ * Iterates running components, runs the picked handler with timeout, and
6178
+ * aggregates per-component results into a SignalBroadcastResult.
5767
6179
  */
5768
- async broadcastReload() {
6180
+ async runSignalBroadcast(descriptor) {
5769
6181
  const results = [];
5770
- const componentsToReload = this.components.filter(
6182
+ const targets = this.components.filter(
5771
6183
  (component) => this.runningComponents.has(component.getName())
5772
6184
  );
5773
6185
  if (this.isStarting) {
5774
- this.logger.info(
5775
- "Reload during startup: only reloading already-started components"
5776
- );
6186
+ this.logger.info(descriptor.startupLog);
5777
6187
  }
5778
- for (const component of componentsToReload) {
6188
+ for (const component of targets) {
5779
6189
  const name = component.getName();
5780
- if (!component.onReload) {
6190
+ const handler = descriptor.pickHandler(component);
6191
+ if (!handler) {
5781
6192
  results.push({
5782
6193
  name,
5783
6194
  called: false,
@@ -5787,13 +6198,13 @@ var LifecycleManager = class extends EventEmitterProtected {
5787
6198
  });
5788
6199
  continue;
5789
6200
  }
5790
- this.lifecycleEvents.componentReloadStarted(name);
6201
+ descriptor.emitStarted(name);
5791
6202
  const timeoutMS = component.signalTimeoutMS;
5792
6203
  let timeoutHandle;
5793
6204
  const timeoutResult = { timedOut: true };
5794
6205
  try {
5795
- const result = component.onReload();
5796
- const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
6206
+ const handlerResult = handler();
6207
+ const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
5797
6208
  const outcome = timeoutMS > 0 ? await Promise.race([
5798
6209
  handlerPromise,
5799
6210
  new Promise((resolve) => {
@@ -5803,7 +6214,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5803
6214
  })
5804
6215
  ]) : await handlerPromise;
5805
6216
  if (outcome === timeoutResult) {
5806
- this.logger.entity(name).warn("Reload handler timed out", {
6217
+ this.logger.entity(name).warn(descriptor.timeoutLog, {
5807
6218
  params: { timeoutMS }
5808
6219
  });
5809
6220
  Promise.resolve(handlerPromise).catch(() => {
@@ -5816,7 +6227,7 @@ var LifecycleManager = class extends EventEmitterProtected {
5816
6227
  code: "timeout"
5817
6228
  });
5818
6229
  } else {
5819
- this.lifecycleEvents.componentReloadCompleted(name);
6230
+ descriptor.emitCompleted(name);
5820
6231
  results.push({
5821
6232
  name,
5822
6233
  called: true,
@@ -5827,10 +6238,10 @@ var LifecycleManager = class extends EventEmitterProtected {
5827
6238
  }
5828
6239
  } catch (error) {
5829
6240
  const err = error instanceof Error ? error : new Error(String(error));
5830
- this.logger.entity(name).error("Reload failed: {{error.message}}", {
6241
+ this.logger.entity(name).error(descriptor.errorLog, {
5831
6242
  params: { error: err }
5832
6243
  });
5833
- this.lifecycleEvents.componentReloadFailed(name, err);
6244
+ descriptor.emitFailed(name, err);
5834
6245
  results.push({
5835
6246
  name,
5836
6247
  called: true,
@@ -5851,108 +6262,45 @@ var LifecycleManager = class extends EventEmitterProtected {
5851
6262
  const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
5852
6263
  const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
5853
6264
  return {
5854
- signal: "reload",
6265
+ signal: descriptor.signal,
5855
6266
  results,
5856
6267
  timedOut: hasTimeout,
5857
6268
  code
5858
6269
  };
5859
6270
  }
6271
+ /**
6272
+ * Broadcast reload signal to all running components.
6273
+ * Calls onReload() on components that implement it.
6274
+ * Continues on errors - collects all results.
6275
+ */
6276
+ async broadcastReload() {
6277
+ return this.runSignalBroadcast({
6278
+ signal: "reload",
6279
+ pickHandler: (component) => component.onReload?.bind(component),
6280
+ startupLog: "Reload during startup: only reloading already-started components",
6281
+ timeoutLog: "Reload handler timed out",
6282
+ errorLog: "Reload failed: {{error.message}}",
6283
+ emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
6284
+ emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
6285
+ emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
6286
+ });
6287
+ }
5860
6288
  /**
5861
6289
  * Broadcast info signal to all running components.
5862
6290
  * Calls onInfo() on components that implement it.
5863
6291
  * Continues on errors - collects all results.
5864
6292
  */
5865
6293
  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 {
6294
+ return this.runSignalBroadcast({
5951
6295
  signal: "info",
5952
- results,
5953
- timedOut: hasTimeout,
5954
- code
5955
- };
6296
+ pickHandler: (component) => component.onInfo?.bind(component),
6297
+ startupLog: "Info during startup: only notifying already-started components",
6298
+ timeoutLog: "Info handler timed out",
6299
+ errorLog: "Info handler failed: {{error.message}}",
6300
+ emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
6301
+ emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
6302
+ emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
6303
+ });
5956
6304
  }
5957
6305
  /**
5958
6306
  * Broadcast debug signal to all running components.
@@ -5960,96 +6308,16 @@ var LifecycleManager = class extends EventEmitterProtected {
5960
6308
  * Continues on errors - collects all results.
5961
6309
  */
5962
6310
  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 {
6311
+ return this.runSignalBroadcast({
6048
6312
  signal: "debug",
6049
- results,
6050
- timedOut: hasTimeout,
6051
- code
6052
- };
6313
+ pickHandler: (component) => component.onDebug?.bind(component),
6314
+ startupLog: "Debug during startup: only notifying already-started components",
6315
+ timeoutLog: "Debug handler timed out",
6316
+ errorLog: "Debug handler failed: {{error.message}}",
6317
+ emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
6318
+ emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
6319
+ emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
6320
+ });
6053
6321
  }
6054
6322
  };
6055
6323
 
@@ -6075,6 +6343,12 @@ var BaseComponent = class {
6075
6343
  name;
6076
6344
  /** Reference to component-scoped lifecycle (set by manager when registered) */
6077
6345
  lifecycle;
6346
+ /** @internal Set by LifecycleManager while the component is running. */
6347
+ _unexpectedStopHandler;
6348
+ /** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
6349
+ _unexpectedStopGeneration = 0;
6350
+ /** @internal Flag indicating whether this component is currently registered with a LifecycleManager */
6351
+ _isRegistered = false;
6078
6352
  /**
6079
6353
  * Create a new component
6080
6354
  *
@@ -6109,6 +6383,37 @@ var BaseComponent = class {
6109
6383
  // Default if undefined/null/non-finite
6110
6384
  );
6111
6385
  }
6386
+ /** @internal Called by LifecycleManager after a successful start. */
6387
+ _setUnexpectedStopHandler(handler) {
6388
+ this._unexpectedStopGeneration += 1;
6389
+ this._unexpectedStopHandler = handler;
6390
+ const generation = this._unexpectedStopGeneration;
6391
+ this.reportUnexpectedStop = (error) => {
6392
+ if (this._unexpectedStopGeneration !== generation) {
6393
+ return false;
6394
+ }
6395
+ return this._unexpectedStopHandler?.(error) ?? false;
6396
+ };
6397
+ }
6398
+ /** @internal Called by LifecycleManager when stop begins or component is unregistered. */
6399
+ _clearUnexpectedStopHandler() {
6400
+ this._unexpectedStopGeneration += 1;
6401
+ this._unexpectedStopHandler = void 0;
6402
+ this.reportUnexpectedStop = () => false;
6403
+ }
6404
+ /** @internal Called by LifecycleManager when registering the component. */
6405
+ _markRegistered() {
6406
+ this._isRegistered = true;
6407
+ }
6408
+ /** @internal Called by LifecycleManager when unregistering the component. */
6409
+ _markUnregistered() {
6410
+ this._isRegistered = false;
6411
+ this.lifecycle = void 0;
6412
+ }
6413
+ /** @internal Check if the component is registered with any LifecycleManager. */
6414
+ _isRegisteredWithManager() {
6415
+ return this._isRegistered;
6416
+ }
6112
6417
  /**
6113
6418
  * Get component name
6114
6419
  */
@@ -6127,6 +6432,38 @@ var BaseComponent = class {
6127
6432
  isOptional() {
6128
6433
  return this.optional;
6129
6434
  }
6435
+ /**
6436
+ * Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
6437
+ * successful start so captured references from older runs go stale.
6438
+ */
6439
+ reportUnexpectedStop = () => false;
6440
+ /**
6441
+ * Get this component's own status from the manager's perspective.
6442
+ *
6443
+ * Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
6444
+ * needing to pass the name. Returns `undefined` if the component is not registered.
6445
+ *
6446
+ * Check `status?.state === 'running'` to test whether the component is currently running.
6447
+ */
6448
+ getSelfStatus() {
6449
+ return this.lifecycle?.getComponentStatus(this.name);
6450
+ }
6451
+ /**
6452
+ * Capture a run-scoped unexpected-stop reporter for async listeners created during start().
6453
+ *
6454
+ * Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
6455
+ * once the component is stopped, unregistered, or restarted. This prevents stale listeners
6456
+ * from a previous run from stopping a newer run of the same component instance.
6457
+ */
6458
+ getUnexpectedStopReporter() {
6459
+ const generation = this._unexpectedStopGeneration;
6460
+ return (error) => {
6461
+ if (this._unexpectedStopGeneration !== generation) {
6462
+ return false;
6463
+ }
6464
+ return this._unexpectedStopHandler?.(error) ?? false;
6465
+ };
6466
+ }
6130
6467
  };
6131
6468
  export {
6132
6469
  BaseComponent,