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.
@@ -956,6 +956,9 @@ var ComponentLifecycle = class {
956
956
  getSignalStatus() {
957
957
  return this.manager.getSignalStatus();
958
958
  }
959
+ getShutdownEscalationStatus() {
960
+ return this.manager.getShutdownEscalationStatus();
961
+ }
959
962
  triggerReload() {
960
963
  return this.manager.triggerReload();
961
964
  }
@@ -1134,6 +1137,15 @@ var LifecycleManagerEvents = class {
1134
1137
  lifecycleManagerShutdownCompleted(input) {
1135
1138
  this.emit("lifecycle-manager:shutdown-completed", input);
1136
1139
  }
1140
+ lifecycleManagerShutdownEscalationArmed(input) {
1141
+ this.emit("lifecycle-manager:shutdown-escalation-armed", input);
1142
+ }
1143
+ lifecycleManagerShutdownEscalationExpired(input) {
1144
+ this.emit("lifecycle-manager:shutdown-escalation-expired", input);
1145
+ }
1146
+ lifecycleManagerShutdownEscalationForced(input) {
1147
+ this.emit("lifecycle-manager:shutdown-escalation-forced", input);
1148
+ }
1137
1149
  componentStarting(name) {
1138
1150
  this.emit("component:starting", { name });
1139
1151
  }
@@ -1210,8 +1222,8 @@ var LifecycleManagerEvents = class {
1210
1222
  componentStartupRollback(name) {
1211
1223
  this.emit("component:startup-rollback", { name });
1212
1224
  }
1213
- signalShutdown(method) {
1214
- this.emit("signal:shutdown", { method });
1225
+ signalShutdown(method, isAlreadyShuttingDown = false) {
1226
+ this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
1215
1227
  }
1216
1228
  signalReload() {
1217
1229
  this.emit("signal:reload", void 0);
@@ -1921,6 +1933,7 @@ var LifecycleManager = class extends EventEmitterProtected {
1921
1933
  attachSignalsBeforeStartup;
1922
1934
  attachSignalsOnStart;
1923
1935
  detachSignalsOnStop;
1936
+ repeatedShutdownRequestPolicy;
1924
1937
  // Component management
1925
1938
  components = [];
1926
1939
  runningComponents = /* @__PURE__ */ new Set();
@@ -1940,6 +1953,17 @@ var LifecycleManager = class extends EventEmitterProtected {
1940
1953
  pendingLoggerExitResolve = null;
1941
1954
  shutdownMethod = null;
1942
1955
  lastShutdownResult = null;
1956
+ repeatedShutdownExpiryTimer = null;
1957
+ repeatedShutdownRequestState = {
1958
+ requestCount: 0,
1959
+ firstMethod: null,
1960
+ latestMethod: null,
1961
+ firstRequestAt: null,
1962
+ latestRequestAt: null,
1963
+ repeatedWindowStartedAt: null,
1964
+ hasTriggeredForceShutdown: false,
1965
+ remainsArmedUntil: null
1966
+ };
1943
1967
  // Signal management
1944
1968
  processSignalManager = null;
1945
1969
  onReloadRequested;
@@ -1966,6 +1990,37 @@ var LifecycleManager = class extends EventEmitterProtected {
1966
1990
  this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
1967
1991
  this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
1968
1992
  this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
1993
+ const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
1994
+ if (repeatedShutdownRequestPolicy === void 0) {
1995
+ this.repeatedShutdownRequestPolicy = void 0;
1996
+ } else {
1997
+ const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
1998
+ repeatedShutdownRequestPolicy.armedAfterFailureMS
1999
+ );
2000
+ const forceAfterCount = finiteClampMin(
2001
+ repeatedShutdownRequestPolicy.forceAfterCount,
2002
+ 1,
2003
+ 3
2004
+ );
2005
+ const withinMS = finiteClampMin(
2006
+ repeatedShutdownRequestPolicy.withinMS,
2007
+ 0,
2008
+ 2e3
2009
+ );
2010
+ const armedAfterFailureMS = finiteClampMin(
2011
+ repeatedShutdownRequestPolicy.armedAfterFailureMS,
2012
+ 0,
2013
+ withinMS * forceAfterCount
2014
+ );
2015
+ this.repeatedShutdownRequestPolicy = {
2016
+ forceAfterCount,
2017
+ withinMS,
2018
+ armedAfterFailureMS,
2019
+ countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
2020
+ hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
2021
+ onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
2022
+ };
2023
+ }
1969
2024
  this.onReloadRequested = options.onReloadRequested;
1970
2025
  this.onInfoRequested = options.onInfoRequested;
1971
2026
  this.onDebugRequested = options.onDebugRequested;
@@ -2534,6 +2589,7 @@ var LifecycleManager = class extends EventEmitterProtected {
2534
2589
  };
2535
2590
  }
2536
2591
  this.isStarting = true;
2592
+ this.resetRepeatedShutdownRequestState();
2537
2593
  this.shutdownMethod = null;
2538
2594
  this.lastShutdownResult = null;
2539
2595
  const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
@@ -2974,6 +3030,51 @@ var LifecycleManager = class extends EventEmitterProtected {
2974
3030
  shutdownMethod: this.shutdownMethod
2975
3031
  };
2976
3032
  }
3033
+ /**
3034
+ * Get status information about repeated shutdown escalation configuration and runtime state.
3035
+ */
3036
+ getShutdownEscalationStatus() {
3037
+ if (this.repeatedShutdownRequestPolicy === void 0) {
3038
+ return {
3039
+ configured: false,
3040
+ isShuttingDown: this.isShuttingDown,
3041
+ isArmed: false,
3042
+ forceAfterCount: null,
3043
+ withinMS: null,
3044
+ armedAfterFailureMS: null,
3045
+ armedAfterFailureMSSource: null,
3046
+ requestCount: 0,
3047
+ firstMethod: null,
3048
+ latestMethod: null,
3049
+ firstRequestAt: null,
3050
+ latestRequestAt: null,
3051
+ repeatedWindowStartedAt: null,
3052
+ armedUntil: null,
3053
+ hasTriggeredForceShutdown: false
3054
+ };
3055
+ }
3056
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
3057
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
3058
+ const isArmed = armedUntil !== null;
3059
+ return {
3060
+ configured: true,
3061
+ isShuttingDown: this.isShuttingDown,
3062
+ isArmed,
3063
+ forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
3064
+ withinMS: this.repeatedShutdownRequestPolicy.withinMS,
3065
+ armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
3066
+ armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
3067
+ countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
3068
+ requestCount: this.repeatedShutdownRequestState.requestCount,
3069
+ firstMethod: this.repeatedShutdownRequestState.firstMethod,
3070
+ latestMethod: this.repeatedShutdownRequestState.latestMethod,
3071
+ firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
3072
+ latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
3073
+ repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
3074
+ armedUntil: isArmed ? armedUntil : null,
3075
+ hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
3076
+ };
3077
+ }
2977
3078
  /**
2978
3079
  * Enable Logger exit hook integration
2979
3080
  *
@@ -3999,9 +4100,26 @@ var LifecycleManager = class extends EventEmitterProtected {
3999
4100
  code: "already_in_progress"
4000
4101
  };
4001
4102
  }
4103
+ this.normalizeRepeatedShutdownRequestStateArmedStatus();
4104
+ const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
4105
+ const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
4106
+ if (isManualRetryWhileArmed) {
4107
+ if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
4108
+ this.handleRepeatedShutdownRequest(method);
4109
+ } else {
4110
+ this.resetRepeatedShutdownRequestState();
4111
+ }
4112
+ }
4113
+ if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
4114
+ this.clearRepeatedShutdownExpiryTimer();
4115
+ this.repeatedShutdownRequestState.remainsArmedUntil = null;
4116
+ }
4002
4117
  this.isShuttingDown = true;
4003
4118
  this.shutdownToken = ulid2();
4004
4119
  this.shutdownMethod = method;
4120
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
4121
+ this.seedRepeatedShutdownRequestState(method);
4122
+ }
4005
4123
  const isDuringStartup = this.isStarting;
4006
4124
  this.logger.info("Stopping all components", { params: { method } });
4007
4125
  this.lifecycleEvents.lifecycleManagerShutdownInitiated(
@@ -4101,15 +4219,26 @@ var LifecycleManager = class extends EventEmitterProtected {
4101
4219
  } else {
4102
4220
  await shutdownOperation();
4103
4221
  }
4222
+ if (!shouldRetryStalled) {
4223
+ 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);
4227
+ }
4228
+ }
4229
+ }
4104
4230
  const durationMS = Date.now() - startTime;
4105
4231
  const isSuccess = !hasTimedOut && stalledComponents.length === 0;
4106
- this.logger[isSuccess ? "success" : "warn"]("Shutdown completed", {
4107
- params: {
4108
- stopped: stoppedComponents.length,
4109
- stalled: stalledComponents.length,
4110
- durationMS
4232
+ this.logger[isSuccess ? "success" : "warn"](
4233
+ isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
4234
+ {
4235
+ params: {
4236
+ stopped: stoppedComponents.length,
4237
+ stalled: stalledComponents.length,
4238
+ durationMS
4239
+ }
4111
4240
  }
4112
- });
4241
+ );
4113
4242
  const result = {
4114
4243
  success: isSuccess,
4115
4244
  stoppedComponents,
@@ -4127,6 +4256,11 @@ var LifecycleManager = class extends EventEmitterProtected {
4127
4256
  method,
4128
4257
  duringStartup: isDuringStartup
4129
4258
  });
4259
+ if (isSuccess) {
4260
+ this.resetRepeatedShutdownRequestState();
4261
+ } else {
4262
+ this.armRepeatedShutdownAfterFailure();
4263
+ }
4130
4264
  return result;
4131
4265
  } finally {
4132
4266
  if (timeoutHandle) {
@@ -5255,21 +5389,295 @@ var LifecycleManager = class extends EventEmitterProtected {
5255
5389
  }
5256
5390
  /**
5257
5391
  * Handle shutdown signal - initiates stopAllComponents().
5258
- * Double signal protection: if already shutting down, log warning and ignore.
5392
+ *
5393
+ * Four cases depending on the current shutdown state:
5394
+ *
5395
+ * 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
5396
+ * repeated-shutdown policy if configured, otherwise log and discard.
5397
+ * Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
5398
+ * without starting another shutdown.
5399
+ *
5400
+ * 2. **Armed post-failure** (previous shutdown finished, armed window still
5401
+ * open): count the request toward the escalation window, emit
5402
+ * `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
5403
+ * new `stopAllComponents()` run to retry.
5404
+ *
5405
+ * 3. **Armed post-failure expired** (armed window opened but has since
5406
+ * elapsed): expire the stale state, treat the request as a fresh
5407
+ * shutdown (falls through to case 4).
5408
+ *
5409
+ * 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
5410
+ * if policy is configured, emit `signal:shutdown` with
5411
+ * `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
5412
+ *
5413
+ * In all cases `signal:shutdown` is emitted exactly once.
5259
5414
  */
5260
5415
  handleShutdownRequest(method) {
5261
5416
  if (this.isShuttingDown) {
5262
- this.logger.warn("Shutdown already in progress, ignoring signal", {
5263
- params: { method }
5264
- });
5265
- return;
5417
+ this.lifecycleEvents.signalShutdown(method, true);
5418
+ if (this.handleRepeatedShutdownRequest(method)) {
5419
+ return;
5420
+ }
5421
+ }
5422
+ let didEmitShutdownSignal = false;
5423
+ let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
5424
+ if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
5425
+ this.lifecycleEvents.signalShutdown(method, false);
5426
+ didEmitShutdownSignal = true;
5427
+ shouldSeedRepeatedShutdownState = false;
5428
+ this.handleRepeatedShutdownRequest(method);
5429
+ }
5430
+ if (shouldSeedRepeatedShutdownState) {
5431
+ this.seedRepeatedShutdownRequestState(method);
5266
5432
  }
5267
5433
  this.logger.info("Shutdown signal received", { params: { method } });
5268
- this.lifecycleEvents.signalShutdown(method);
5434
+ if (!didEmitShutdownSignal) {
5435
+ this.lifecycleEvents.signalShutdown(method, false);
5436
+ }
5269
5437
  void this.stopAllComponentsInternal(method, {
5270
5438
  ...this.shutdownOptions
5271
5439
  });
5272
5440
  }
5441
+ /**
5442
+ * Tracks repeated shutdown requests during an active shutdown and optionally
5443
+ * invokes the configured force shutdown callback when the threshold is reached.
5444
+ *
5445
+ * @returns true when the request was consumed as part of the repeated-shutdown
5446
+ * escalation flow, false when the caller should treat it as a fresh shutdown request
5447
+ */
5448
+ handleRepeatedShutdownRequest(method) {
5449
+ const policy = this.repeatedShutdownRequestPolicy;
5450
+ if (!policy) {
5451
+ this.logger.warn("Shutdown already in progress, ignoring signal", {
5452
+ params: { method }
5453
+ });
5454
+ return true;
5455
+ }
5456
+ const now = Date.now();
5457
+ const state = this.repeatedShutdownRequestState;
5458
+ if (state.remainsArmedUntil !== null) {
5459
+ if (now >= state.remainsArmedUntil) {
5460
+ this.expireRepeatedShutdownRequestState();
5461
+ return false;
5462
+ }
5463
+ this.refreshRepeatedShutdownArmedWindow(now);
5464
+ }
5465
+ const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
5466
+ if (shouldStartNewWindow) {
5467
+ state.requestCount = 1;
5468
+ state.repeatedWindowStartedAt = now;
5469
+ } else {
5470
+ state.requestCount++;
5471
+ }
5472
+ state.latestMethod = method;
5473
+ state.latestRequestAt = now;
5474
+ this.logger.warn(
5475
+ 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",
5476
+ {
5477
+ params: {
5478
+ method,
5479
+ requestCount: state.requestCount,
5480
+ firstMethod: state.firstMethod,
5481
+ latestMethod: state.latestMethod,
5482
+ firstRequestAt: state.firstRequestAt,
5483
+ latestRequestAt: state.latestRequestAt,
5484
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5485
+ remainsArmedUntil: state.remainsArmedUntil,
5486
+ withinMS: policy.withinMS,
5487
+ forceAfterCount: policy.forceAfterCount
5488
+ }
5489
+ }
5490
+ );
5491
+ if (
5492
+ // Force escalation is single-fire per shutdown cycle. Later requests are
5493
+ // still logged but do not re-enter user force-shutdown logic.
5494
+ state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
5495
+ ) {
5496
+ return true;
5497
+ }
5498
+ state.hasTriggeredForceShutdown = true;
5499
+ const context = {
5500
+ requestCount: state.requestCount,
5501
+ firstMethod: state.firstMethod,
5502
+ latestMethod: state.latestMethod,
5503
+ firstRequestAt: state.firstRequestAt,
5504
+ latestRequestAt: state.latestRequestAt,
5505
+ isShuttingDown: this.isShuttingDown,
5506
+ wasArmedAfterFailure: state.remainsArmedUntil !== null
5507
+ };
5508
+ this.logger.warn(
5509
+ "Repeated shutdown request threshold reached, invoking force shutdown handler",
5510
+ {
5511
+ params: {
5512
+ method,
5513
+ requestCount: context.requestCount,
5514
+ firstMethod: context.firstMethod,
5515
+ latestMethod: context.latestMethod,
5516
+ firstRequestAt: context.firstRequestAt,
5517
+ latestRequestAt: context.latestRequestAt,
5518
+ repeatedWindowStartedAt: state.repeatedWindowStartedAt,
5519
+ remainsArmedUntil: state.remainsArmedUntil,
5520
+ withinMS: policy.withinMS,
5521
+ forceAfterCount: policy.forceAfterCount
5522
+ }
5523
+ }
5524
+ );
5525
+ safeHandleCallback(
5526
+ "repeatedShutdownRequestPolicy.onForceShutdown",
5527
+ policy.onForceShutdown,
5528
+ context
5529
+ );
5530
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
5531
+ firstMethod: context.firstMethod,
5532
+ latestMethod: context.latestMethod,
5533
+ requestCount: context.requestCount,
5534
+ firstRequestAt: context.firstRequestAt,
5535
+ latestRequestAt: context.latestRequestAt,
5536
+ wasArmedAfterFailure: context.wasArmedAfterFailure
5537
+ });
5538
+ return true;
5539
+ }
5540
+ /**
5541
+ * Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
5542
+ */
5543
+ resetRepeatedShutdownRequestState() {
5544
+ this.clearRepeatedShutdownExpiryTimer();
5545
+ this.repeatedShutdownRequestState = {
5546
+ requestCount: 0,
5547
+ firstMethod: null,
5548
+ latestMethod: null,
5549
+ firstRequestAt: null,
5550
+ latestRequestAt: null,
5551
+ repeatedWindowStartedAt: null,
5552
+ hasTriggeredForceShutdown: false,
5553
+ remainsArmedUntil: null
5554
+ };
5555
+ }
5556
+ /**
5557
+ * Clear any pending expiration timer for the post-failure escalation window.
5558
+ */
5559
+ clearRepeatedShutdownExpiryTimer() {
5560
+ if (this.repeatedShutdownExpiryTimer === null) {
5561
+ return;
5562
+ }
5563
+ clearTimeout(this.repeatedShutdownExpiryTimer);
5564
+ this.repeatedShutdownExpiryTimer = null;
5565
+ }
5566
+ /**
5567
+ * Returns whether post-failure escalation remains armed after first
5568
+ * normalizing any stale timer-backed state.
5569
+ *
5570
+ * The method can expire old armed windows as a side effect because the timer
5571
+ * callback may not have run yet on a delayed event loop. Callers use this
5572
+ * when they need the effective runtime truth, not just the last timer write.
5573
+ */
5574
+ normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
5575
+ const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
5576
+ if (armedUntil === null) {
5577
+ return false;
5578
+ }
5579
+ if (now >= armedUntil) {
5580
+ this.expireRepeatedShutdownRequestState();
5581
+ return false;
5582
+ }
5583
+ return true;
5584
+ }
5585
+ /**
5586
+ * Transition armed post-failure escalation state into its expired/reset state.
5587
+ */
5588
+ expireRepeatedShutdownRequestState() {
5589
+ const policy = this.repeatedShutdownRequestPolicy;
5590
+ const state = this.repeatedShutdownRequestState;
5591
+ if (!policy || state.remainsArmedUntil === null) {
5592
+ return;
5593
+ }
5594
+ const armedUntil = state.remainsArmedUntil;
5595
+ this.clearRepeatedShutdownExpiryTimer();
5596
+ const expiredState = {
5597
+ firstMethod: state.firstMethod,
5598
+ latestMethod: state.latestMethod,
5599
+ requestCount: state.requestCount,
5600
+ armedUntil
5601
+ };
5602
+ this.logger.warn(
5603
+ "Repeated shutdown escalation window expired, clearing previous shutdown state",
5604
+ {
5605
+ params: {
5606
+ remainsArmedUntil: armedUntil,
5607
+ withinMS: policy.withinMS,
5608
+ forceAfterCount: policy.forceAfterCount
5609
+ }
5610
+ }
5611
+ );
5612
+ if (expiredState.firstMethod !== null) {
5613
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
5614
+ firstMethod: expiredState.firstMethod,
5615
+ latestMethod: expiredState.latestMethod,
5616
+ requestCount: expiredState.requestCount,
5617
+ armedUntil: expiredState.armedUntil
5618
+ });
5619
+ }
5620
+ this.resetRepeatedShutdownRequestState();
5621
+ }
5622
+ /**
5623
+ * Arms or refreshes the post-failure escalation window and its expiration timer.
5624
+ */
5625
+ refreshRepeatedShutdownArmedWindow(now = Date.now()) {
5626
+ const policy = this.repeatedShutdownRequestPolicy;
5627
+ if (!policy) {
5628
+ return;
5629
+ }
5630
+ this.clearRepeatedShutdownExpiryTimer();
5631
+ const armedUntil = now + policy.armedAfterFailureMS;
5632
+ this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
5633
+ this.repeatedShutdownExpiryTimer = setTimeout(() => {
5634
+ this.expireRepeatedShutdownRequestState();
5635
+ }, policy.armedAfterFailureMS);
5636
+ this.repeatedShutdownExpiryTimer.unref();
5637
+ }
5638
+ /**
5639
+ * Seeds shutdown escalation tracking for a new shutdown cycle.
5640
+ *
5641
+ * The first shutdown trigger starts graceful shutdown and arms escalation with
5642
+ * an effective post-start count of 0. Later shutdown requests can then count
5643
+ * toward the configured force threshold regardless of whether the shutdown
5644
+ * started from a signal, keyboard shortcut, or direct API call.
5645
+ */
5646
+ seedRepeatedShutdownRequestState(method) {
5647
+ const now = Date.now();
5648
+ this.repeatedShutdownRequestState = {
5649
+ requestCount: 0,
5650
+ firstMethod: method,
5651
+ latestMethod: method,
5652
+ firstRequestAt: now,
5653
+ latestRequestAt: now,
5654
+ repeatedWindowStartedAt: null,
5655
+ hasTriggeredForceShutdown: false,
5656
+ remainsArmedUntil: null
5657
+ };
5658
+ }
5659
+ /**
5660
+ * Preserves a short-lived post-failure escalation window after shutdown
5661
+ * returns unsuccessfully so operators can keep pressing shutdown without
5662
+ * losing the existing force count the moment the graceful attempt finishes.
5663
+ */
5664
+ armRepeatedShutdownAfterFailure() {
5665
+ const policy = this.repeatedShutdownRequestPolicy;
5666
+ const state = this.repeatedShutdownRequestState;
5667
+ if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
5668
+ state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
5669
+ return;
5670
+ }
5671
+ this.refreshRepeatedShutdownArmedWindow();
5672
+ const armedUntil = state.remainsArmedUntil;
5673
+ if (state.firstMethod !== null && armedUntil !== null) {
5674
+ this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
5675
+ firstMethod: state.firstMethod,
5676
+ requestCount: state.requestCount,
5677
+ armedUntil
5678
+ });
5679
+ }
5680
+ }
5273
5681
  /**
5274
5682
  * Handle reload request - calls custom callback or broadcasts to components.
5275
5683
  *