lifecycleion 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Lifecycleion v0.0.12
1
+ # Lifecycleion v0.0.13
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/lifecycleion.svg)](https://badge.fury.io/js/lifecycleion)
4
4
 
@@ -1006,6 +1006,9 @@ var ComponentLifecycle = class {
1006
1006
  getSignalStatus() {
1007
1007
  return this.manager.getSignalStatus();
1008
1008
  }
1009
+ getShutdownEscalationStatus() {
1010
+ return this.manager.getShutdownEscalationStatus();
1011
+ }
1009
1012
  triggerReload() {
1010
1013
  return this.manager.triggerReload();
1011
1014
  }
@@ -1184,6 +1187,15 @@ var LifecycleManagerEvents = class {
1184
1187
  lifecycleManagerShutdownCompleted(input) {
1185
1188
  this.emit("lifecycle-manager:shutdown-completed", input);
1186
1189
  }
1190
+ lifecycleManagerShutdownEscalationArmed(input) {
1191
+ this.emit("lifecycle-manager:shutdown-escalation-armed", input);
1192
+ }
1193
+ lifecycleManagerShutdownEscalationExpired(input) {
1194
+ this.emit("lifecycle-manager:shutdown-escalation-expired", input);
1195
+ }
1196
+ lifecycleManagerShutdownEscalationForced(input) {
1197
+ this.emit("lifecycle-manager:shutdown-escalation-forced", input);
1198
+ }
1187
1199
  componentStarting(name) {
1188
1200
  this.emit("component:starting", { name });
1189
1201
  }
@@ -1260,8 +1272,8 @@ var LifecycleManagerEvents = class {
1260
1272
  componentStartupRollback(name) {
1261
1273
  this.emit("component:startup-rollback", { name });
1262
1274
  }
1263
- signalShutdown(method) {
1264
- this.emit("signal:shutdown", { method });
1275
+ signalShutdown(method, isAlreadyShuttingDown = false) {
1276
+ this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
1265
1277
  }
1266
1278
  signalReload() {
1267
1279
  this.emit("signal:reload", void 0);
@@ -1971,6 +1983,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1971
1983
  attachSignalsBeforeStartup;
1972
1984
  attachSignalsOnStart;
1973
1985
  detachSignalsOnStop;
1986
+ repeatedShutdownRequestPolicy;
1974
1987
  // Component management
1975
1988
  components = [];
1976
1989
  runningComponents = /* @__PURE__ */ new Set();
@@ -1990,6 +2003,17 @@ var LifecycleManager = class extends EventEmitterProtected {
1990
2003
  pendingLoggerExitResolve = null;
1991
2004
  shutdownMethod = null;
1992
2005
  lastShutdownResult = null;
2006
+ repeatedShutdownExpiryTimer = null;
2007
+ repeatedShutdownRequestState = {
2008
+ requestCount: 0,
2009
+ firstMethod: null,
2010
+ latestMethod: null,
2011
+ firstRequestAt: null,
2012
+ latestRequestAt: null,
2013
+ repeatedWindowStartedAt: null,
2014
+ hasTriggeredForceShutdown: false,
2015
+ remainsArmedUntil: null
2016
+ };
1993
2017
  // Signal management
1994
2018
  processSignalManager = null;
1995
2019
  onReloadRequested;
@@ -2016,6 +2040,37 @@ var LifecycleManager = class extends EventEmitterProtected {
2016
2040
  this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
2017
2041
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
2018
2042
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
2043
+ const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
2044
+ if (repeatedShutdownRequestPolicy === void 0) {
2045
+ this.repeatedShutdownRequestPolicy = void 0;
2046
+ } else {
2047
+ const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
2048
+ repeatedShutdownRequestPolicy.armedAfterFailureMS
2049
+ );
2050
+ const forceAfterCount = finiteClampMin(
2051
+ repeatedShutdownRequestPolicy.forceAfterCount,
2052
+ 1,
2053
+ 3
2054
+ );
2055
+ const withinMS = finiteClampMin(
2056
+ repeatedShutdownRequestPolicy.withinMS,
2057
+ 0,
2058
+ 2e3
2059
+ );
2060
+ const armedAfterFailureMS = finiteClampMin(
2061
+ repeatedShutdownRequestPolicy.armedAfterFailureMS,
2062
+ 0,
2063
+ withinMS * forceAfterCount
2064
+ );
2065
+ this.repeatedShutdownRequestPolicy = {
2066
+ forceAfterCount,
2067
+ withinMS,
2068
+ armedAfterFailureMS,
2069
+ countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
2070
+ hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
2071
+ onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
2072
+ };
2073
+ }
2019
2074
  this.onReloadRequested = options.onReloadRequested;
2020
2075
  this.onInfoRequested = options.onInfoRequested;
2021
2076
  this.onDebugRequested = options.onDebugRequested;
@@ -2584,6 +2639,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2584
2639
  };
2585
2640
  }
2586
2641
  this.isStarting = true;
2642
+ this.resetRepeatedShutdownRequestState();
2587
2643
  this.shutdownMethod = null;
2588
2644
  this.lastShutdownResult = null;
2589
2645
  const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
@@ -3024,6 +3080,51 @@ var LifecycleManager = class extends EventEmitterProtected {
3024
3080
  shutdownMethod: this.shutdownMethod
3025
3081
  };
3026
3082
  }
3083
+ /**
3084
+ * Get status information about repeated shutdown escalation configuration and runtime state.
3085
+ */
3086
+ getShutdownEscalationStatus() {
3087
+ if (this.repeatedShutdownRequestPolicy === void 0) {
3088
+ return {
3089
+ configured: false,
3090
+ isShuttingDown: this.isShuttingDown,
3091
+ isArmed: false,
3092
+ forceAfterCount: null,
3093
+ withinMS: null,
3094
+ armedAfterFailureMS: null,
3095
+ armedAfterFailureMSSource: null,
3096
+ requestCount: 0,
3097
+ firstMethod: null,
3098
+ latestMethod: null,
3099
+ firstRequestAt: null,
3100
+ latestRequestAt: null,
3101
+ repeatedWindowStartedAt: null,
3102
+ armedUntil: null,
3103
+ hasTriggeredForceShutdown: false
3104
+ };
3105
+ }
3106
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
3107
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
3108
+ const isArmed = armedUntil !== null;
3109
+ return {
3110
+ configured: true,
3111
+ isShuttingDown: this.isShuttingDown,
3112
+ isArmed,
3113
+ forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
3114
+ withinMS: this.repeatedShutdownRequestPolicy.withinMS,
3115
+ armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
3116
+ armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
3117
+ countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
3118
+ requestCount: this.repeatedShutdownRequestState.requestCount,
3119
+ firstMethod: this.repeatedShutdownRequestState.firstMethod,
3120
+ latestMethod: this.repeatedShutdownRequestState.latestMethod,
3121
+ firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
3122
+ latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
3123
+ repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
3124
+ armedUntil: isArmed ? armedUntil : null,
3125
+ hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
3126
+ };
3127
+ }
3027
3128
  /**
3028
3129
  * Enable Logger exit hook integration
3029
3130
  *
@@ -4049,9 +4150,26 @@ var LifecycleManager = class extends EventEmitterProtected {
4049
4150
  code: "already_in_progress"
4050
4151
  };
4051
4152
  }
4153
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
4154
+ const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
4155
+ const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
4156
+ if (isManualRetryWhileArmed) {
4157
+ if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
4158
+ this.handleRepeatedShutdownRequest(method);
4159
+ } else {
4160
+ this.resetRepeatedShutdownRequestState();
4161
+ }
4162
+ }
4163
+ if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
4164
+ this.clearRepeatedShutdownExpiryTimer();
4165
+ this.repeatedShutdownRequestState.remainsArmedUntil = null;
4166
+ }
4052
4167
  this.isShuttingDown = true;
4053
4168
  this.shutdownToken = (0, import_ulid2.ulid)();
4054
4169
  this.shutdownMethod = method;
4170
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
4171
+ this.seedRepeatedShutdownRequestState(method);
4172
+ }
4055
4173
  const isDuringStartup = this.isStarting;
4056
4174
  this.logger.info("Stopping all components", { params: { method } });
4057
4175
  this.lifecycleEvents.lifecycleManagerShutdownInitiated(
@@ -4151,15 +4269,26 @@ var LifecycleManager = class extends EventEmitterProtected {
4151
4269
  } else {
4152
4270
  await shutdownOperation();
4153
4271
  }
4272
+ if (!shouldRetryStalled) {
4273
+ for (const name of stalledComponentNames) {
4274
+ const stallInfo = this.stalledComponents.get(name);
4275
+ if (stallInfo && !stalledComponents.some((component) => component.name === name)) {
4276
+ stalledComponents.push(stallInfo);
4277
+ }
4278
+ }
4279
+ }
4154
4280
  const durationMS = Date.now() - startTime;
4155
4281
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4156
- this.logger[isSuccess ? "success" : "warn"]("Shutdown completed", {
4157
- params: {
4158
- stopped: stoppedComponents.length,
4159
- stalled: stalledComponents.length,
4160
- durationMS
4282
+ this.logger[isSuccess ? "success" : "warn"](
4283
+ isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4284
+ {
4285
+ params: {
4286
+ stopped: stoppedComponents.length,
4287
+ stalled: stalledComponents.length,
4288
+ durationMS
4289
+ }
4161
4290
  }
4162
- });
4291
+ );
4163
4292
  const result = {
4164
4293
  success: isSuccess,
4165
4294
  stoppedComponents,
@@ -4177,6 +4306,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4177
4306
  method,
4178
4307
  duringStartup: isDuringStartup
4179
4308
  });
4309
+ if (isSuccess) {
4310
+ this.resetRepeatedShutdownRequestState();
4311
+ } else {
4312
+ this.armRepeatedShutdownAfterFailure();
4313
+ }
4180
4314
  return result;
4181
4315
  } finally {
4182
4316
  if (timeoutHandle) {
@@ -5305,21 +5439,295 @@ var LifecycleManager = class extends EventEmitterProtected {
5305
5439
  }
5306
5440
  /**
5307
5441
  * Handle shutdown signal - initiates stopAllComponents().
5308
- * Double signal protection: if already shutting down, log warning and ignore.
5442
+ *
5443
+ * Four cases depending on the current shutdown state:
5444
+ *
5445
+ * 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
5446
+ * repeated-shutdown policy if configured, otherwise log and discard.
5447
+ * Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
5448
+ * without starting another shutdown.
5449
+ *
5450
+ * 2. **Armed post-failure** (previous shutdown finished, armed window still
5451
+ * open): count the request toward the escalation window, emit
5452
+ * `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
5453
+ * new `stopAllComponents()` run to retry.
5454
+ *
5455
+ * 3. **Armed post-failure expired** (armed window opened but has since
5456
+ * elapsed): expire the stale state, treat the request as a fresh
5457
+ * shutdown (falls through to case 4).
5458
+ *
5459
+ * 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
5460
+ * if policy is configured, emit `signal:shutdown` with
5461
+ * `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
5462
+ *
5463
+ * In all cases `signal:shutdown` is emitted exactly once.
5309
5464
  */
5310
5465
  handleShutdownRequest(method) {
5311
5466
  if (this.isShuttingDown) {
5312
- this.logger.warn("Shutdown already in progress, ignoring signal", {
5313
- params: { method }
5314
- });
5315
- return;
5467
+ this.lifecycleEvents.signalShutdown(method, true);
5468
+ if (this.handleRepeatedShutdownRequest(method)) {
5469
+ return;
5470
+ }
5471
+ }
5472
+ let didEmitShutdownSignal = false;
5473
+ let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
5474
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
5475
+ this.lifecycleEvents.signalShutdown(method, false);
5476
+ didEmitShutdownSignal = true;
5477
+ shouldSeedRepeatedShutdownState = false;
5478
+ this.handleRepeatedShutdownRequest(method);
5479
+ }
5480
+ if (shouldSeedRepeatedShutdownState) {
5481
+ this.seedRepeatedShutdownRequestState(method);
5316
5482
  }
5317
5483
  this.logger.info("Shutdown signal received", { params: { method } });
5318
- this.lifecycleEvents.signalShutdown(method);
5484
+ if (!didEmitShutdownSignal) {
5485
+ this.lifecycleEvents.signalShutdown(method, false);
5486
+ }
5319
5487
  void this.stopAllComponentsInternal(method, {
5320
5488
  ...this.shutdownOptions
5321
5489
  });
5322
5490
  }
5491
+ /**
5492
+ * Tracks repeated shutdown requests during an active shutdown and optionally
5493
+ * invokes the configured force shutdown callback when the threshold is reached.
5494
+ *
5495
+ * @returns true when the request was consumed as part of the repeated-shutdown
5496
+ * escalation flow, false when the caller should treat it as a fresh shutdown request
5497
+ */
5498
+ handleRepeatedShutdownRequest(method) {
5499
+ const policy = this.repeatedShutdownRequestPolicy;
5500
+ if (!policy) {
5501
+ this.logger.warn("Shutdown already in progress, ignoring signal", {
5502
+ params: { method }
5503
+ });
5504
+ return true;
5505
+ }
5506
+ const now = Date.now();
5507
+ const state = this.repeatedShutdownRequestState;
5508
+ if (state.remainsArmedUntil !== null) {
5509
+ if (now >= state.remainsArmedUntil) {
5510
+ this.expireRepeatedShutdownRequestState();
5511
+ return false;
5512
+ }
5513
+ this.refreshRepeatedShutdownArmedWindow(now);
5514
+ }
5515
+ const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
5516
+ if (shouldStartNewWindow) {
5517
+ state.requestCount = 1;
5518
+ state.repeatedWindowStartedAt = now;
5519
+ } else {
5520
+ state.requestCount++;
5521
+ }
5522
+ state.latestMethod = method;
5523
+ state.latestRequestAt = now;
5524
+ this.logger.warn(
5525
+ this.isShuttingDown ? "Shutdown already in progress, tracking repeated signal" : "Previous shutdown attempt finished with stalled components or timeout, escalation window still armed, tracking repeated request",
5526
+ {
5527
+ params: {
5528
+ method,
5529
+ requestCount: state.requestCount,
5530
+ firstMethod: state.firstMethod,
5531
+ latestMethod: state.latestMethod,
5532
+ firstRequestAt: state.firstRequestAt,
5533
+ latestRequestAt: state.latestRequestAt,
5534
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5535
+ remainsArmedUntil: state.remainsArmedUntil,
5536
+ withinMS: policy.withinMS,
5537
+ forceAfterCount: policy.forceAfterCount
5538
+ }
5539
+ }
5540
+ );
5541
+ if (
5542
+ // Force escalation is single-fire per shutdown cycle. Later requests are
5543
+ // still logged but do not re-enter user force-shutdown logic.
5544
+ state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
5545
+ ) {
5546
+ return true;
5547
+ }
5548
+ state.hasTriggeredForceShutdown = true;
5549
+ const context = {
5550
+ requestCount: state.requestCount,
5551
+ firstMethod: state.firstMethod,
5552
+ latestMethod: state.latestMethod,
5553
+ firstRequestAt: state.firstRequestAt,
5554
+ latestRequestAt: state.latestRequestAt,
5555
+ isShuttingDown: this.isShuttingDown,
5556
+ wasArmedAfterFailure: state.remainsArmedUntil !== null
5557
+ };
5558
+ this.logger.warn(
5559
+ "Repeated shutdown request threshold reached, invoking force shutdown handler",
5560
+ {
5561
+ params: {
5562
+ method,
5563
+ requestCount: context.requestCount,
5564
+ firstMethod: context.firstMethod,
5565
+ latestMethod: context.latestMethod,
5566
+ firstRequestAt: context.firstRequestAt,
5567
+ latestRequestAt: context.latestRequestAt,
5568
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5569
+ remainsArmedUntil: state.remainsArmedUntil,
5570
+ withinMS: policy.withinMS,
5571
+ forceAfterCount: policy.forceAfterCount
5572
+ }
5573
+ }
5574
+ );
5575
+ safeHandleCallback(
5576
+ "repeatedShutdownRequestPolicy.onForceShutdown",
5577
+ policy.onForceShutdown,
5578
+ context
5579
+ );
5580
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
5581
+ firstMethod: context.firstMethod,
5582
+ latestMethod: context.latestMethod,
5583
+ requestCount: context.requestCount,
5584
+ firstRequestAt: context.firstRequestAt,
5585
+ latestRequestAt: context.latestRequestAt,
5586
+ wasArmedAfterFailure: context.wasArmedAfterFailure
5587
+ });
5588
+ return true;
5589
+ }
5590
+ /**
5591
+ * Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
5592
+ */
5593
+ resetRepeatedShutdownRequestState() {
5594
+ this.clearRepeatedShutdownExpiryTimer();
5595
+ this.repeatedShutdownRequestState = {
5596
+ requestCount: 0,
5597
+ firstMethod: null,
5598
+ latestMethod: null,
5599
+ firstRequestAt: null,
5600
+ latestRequestAt: null,
5601
+ repeatedWindowStartedAt: null,
5602
+ hasTriggeredForceShutdown: false,
5603
+ remainsArmedUntil: null
5604
+ };
5605
+ }
5606
+ /**
5607
+ * Clear any pending expiration timer for the post-failure escalation window.
5608
+ */
5609
+ clearRepeatedShutdownExpiryTimer() {
5610
+ if (this.repeatedShutdownExpiryTimer === null) {
5611
+ return;
5612
+ }
5613
+ clearTimeout(this.repeatedShutdownExpiryTimer);
5614
+ this.repeatedShutdownExpiryTimer = null;
5615
+ }
5616
+ /**
5617
+ * Returns whether post-failure escalation remains armed after first
5618
+ * normalizing any stale timer-backed state.
5619
+ *
5620
+ * The method can expire old armed windows as a side effect because the timer
5621
+ * callback may not have run yet on a delayed event loop. Callers use this
5622
+ * when they need the effective runtime truth, not just the last timer write.
5623
+ */
5624
+ normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
5625
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
5626
+ if (armedUntil === null) {
5627
+ return false;
5628
+ }
5629
+ if (now >= armedUntil) {
5630
+ this.expireRepeatedShutdownRequestState();
5631
+ return false;
5632
+ }
5633
+ return true;
5634
+ }
5635
+ /**
5636
+ * Transition armed post-failure escalation state into its expired/reset state.
5637
+ */
5638
+ expireRepeatedShutdownRequestState() {
5639
+ const policy = this.repeatedShutdownRequestPolicy;
5640
+ const state = this.repeatedShutdownRequestState;
5641
+ if (!policy || state.remainsArmedUntil === null) {
5642
+ return;
5643
+ }
5644
+ const armedUntil = state.remainsArmedUntil;
5645
+ this.clearRepeatedShutdownExpiryTimer();
5646
+ const expiredState = {
5647
+ firstMethod: state.firstMethod,
5648
+ latestMethod: state.latestMethod,
5649
+ requestCount: state.requestCount,
5650
+ armedUntil
5651
+ };
5652
+ this.logger.warn(
5653
+ "Repeated shutdown escalation window expired, clearing previous shutdown state",
5654
+ {
5655
+ params: {
5656
+ remainsArmedUntil: armedUntil,
5657
+ withinMS: policy.withinMS,
5658
+ forceAfterCount: policy.forceAfterCount
5659
+ }
5660
+ }
5661
+ );
5662
+ if (expiredState.firstMethod !== null) {
5663
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
5664
+ firstMethod: expiredState.firstMethod,
5665
+ latestMethod: expiredState.latestMethod,
5666
+ requestCount: expiredState.requestCount,
5667
+ armedUntil: expiredState.armedUntil
5668
+ });
5669
+ }
5670
+ this.resetRepeatedShutdownRequestState();
5671
+ }
5672
+ /**
5673
+ * Arms or refreshes the post-failure escalation window and its expiration timer.
5674
+ */
5675
+ refreshRepeatedShutdownArmedWindow(now = Date.now()) {
5676
+ const policy = this.repeatedShutdownRequestPolicy;
5677
+ if (!policy) {
5678
+ return;
5679
+ }
5680
+ this.clearRepeatedShutdownExpiryTimer();
5681
+ const armedUntil = now + policy.armedAfterFailureMS;
5682
+ this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
5683
+ this.repeatedShutdownExpiryTimer = setTimeout(() => {
5684
+ this.expireRepeatedShutdownRequestState();
5685
+ }, policy.armedAfterFailureMS);
5686
+ this.repeatedShutdownExpiryTimer.unref();
5687
+ }
5688
+ /**
5689
+ * Seeds shutdown escalation tracking for a new shutdown cycle.
5690
+ *
5691
+ * The first shutdown trigger starts graceful shutdown and arms escalation with
5692
+ * an effective post-start count of 0. Later shutdown requests can then count
5693
+ * toward the configured force threshold regardless of whether the shutdown
5694
+ * started from a signal, keyboard shortcut, or direct API call.
5695
+ */
5696
+ seedRepeatedShutdownRequestState(method) {
5697
+ const now = Date.now();
5698
+ this.repeatedShutdownRequestState = {
5699
+ requestCount: 0,
5700
+ firstMethod: method,
5701
+ latestMethod: method,
5702
+ firstRequestAt: now,
5703
+ latestRequestAt: now,
5704
+ repeatedWindowStartedAt: null,
5705
+ hasTriggeredForceShutdown: false,
5706
+ remainsArmedUntil: null
5707
+ };
5708
+ }
5709
+ /**
5710
+ * Preserves a short-lived post-failure escalation window after shutdown
5711
+ * returns unsuccessfully so operators can keep pressing shutdown without
5712
+ * losing the existing force count the moment the graceful attempt finishes.
5713
+ */
5714
+ armRepeatedShutdownAfterFailure() {
5715
+ const policy = this.repeatedShutdownRequestPolicy;
5716
+ const state = this.repeatedShutdownRequestState;
5717
+ if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
5718
+ state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
5719
+ return;
5720
+ }
5721
+ this.refreshRepeatedShutdownArmedWindow();
5722
+ const armedUntil = state.remainsArmedUntil;
5723
+ if (state.firstMethod !== null && armedUntil !== null) {
5724
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
5725
+ firstMethod: state.firstMethod,
5726
+ requestCount: state.requestCount,
5727
+ armedUntil
5728
+ });
5729
+ }
5730
+ }
5323
5731
  /**
5324
5732
  * Handle reload request - calls custom callback or broadcasts to components.
5325
5733
  *