()
| 103 | let _stateLoaded = false; |
| 104 | |
| 105 | function _loadStateFromDisk() { |
| 106 | if (_stateLoaded) return; |
| 107 | _stateLoaded = true; |
| 108 | try { |
| 109 | const file = _stateFile(); |
| 110 | if (!fs.existsSync(file)) return; |
| 111 | const raw = fs.readFileSync(file, 'utf8'); |
| 112 | if (!raw) return; |
| 113 | const parsed = JSON.parse(raw); |
| 114 | if (!parsed || typeof parsed !== 'object') return; |
| 115 | const nextAttemptAt = Number(parsed.nextAttemptAt) || 0; |
| 116 | const transientFailures = Number(parsed.transientFailures) || 0; |
| 117 | const fundsFailures = Number(parsed.fundsFailures) || 0; |
| 118 | const lastSuccessAt = Number(parsed.lastSuccessAt) || 0; |
| 119 | // Defensive clamp: a clock change or corrupted file could push |
| 120 | // nextAttemptAt arbitrarily far into the future. Cap it at now + |
| 121 | // the longest backoff step to avoid permanently silencing stake |
| 122 | // attempts on this host. |
| 123 | const maxHorizonMs = 24 * 60 * 60 * 1000; |
| 124 | const nowMs = Date.now(); |
| 125 | const boundedNext = nextAttemptAt > nowMs + maxHorizonMs |
| 126 | ? nowMs + maxHorizonMs |
| 127 | : nextAttemptAt; |
| 128 | _state.nextAttemptAt = boundedNext; |
| 129 | _state.transientFailures = transientFailures; |
| 130 | _state.fundsFailures = fundsFailures; |
| 131 | _state.lastSuccessAt = lastSuccessAt; |
| 132 | // disabledUntilRestart is intentionally NOT persisted: we want the |
| 133 | // next process start to re-try once, so operator-driven fixes (hub |
| 134 | // upgrade, client downgrade/reconfig) clear the flag naturally. |
| 135 | } catch (_) { |
| 136 | // best-effort: fall back to default _state |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | function _persistState() { |
| 141 | // NOTE(windows): mode 0o700 / 0o600 are silently ignored on Windows. |
no test coverage detected