Retry peer reconnect every 5s until back online
Release rolling / release (push) Has been cancelled

Replace exponential backoff and the 12-attempt cap with a fixed 5s
retry loop that keeps dialing after unexpected disconnects. Throttle
toasts so the UI is not spammed while the offline banner stays up.
This commit is contained in:
Raven Scott
2026-07-11 17:19:09 -04:00
parent 3fe050d7a6
commit 54c09b5eab
4 changed files with 51 additions and 11 deletions
+17 -3
View File
@@ -73,21 +73,35 @@ manager.on('connect', (conn) => {
); );
} }
}); });
/** Throttle reconnect toasts — retries every 5s forever; don't spam the UI */
let lastReconnectToastAt = 0;
manager.on('reconnecting', ({ attempt, delayMs }) => { manager.on('reconnecting', ({ attempt, delayMs }) => {
updateStatusIndicator(`Reconnecting (attempt ${attempt}) in ${Math.round(delayMs / 1000)}s…`); const secs = Math.max(1, Math.round((delayMs || 5000) / 1000));
if (typeof showAlert === 'function') { updateStatusIndicator(`Reconnecting to peer (attempt ${attempt})… next try in ${secs}s`);
showAlert('warning', `Connection lost — reconnecting (attempt ${attempt})…`); const now = Date.now();
// First attempt + at most once per minute thereafter
if (typeof showAlert === 'function' && (attempt === 1 || now - lastReconnectToastAt > 60_000)) {
lastReconnectToastAt = now;
showAlert(
'warning',
attempt === 1
? 'Connection lost — retrying every 5 seconds…'
: `Still reconnecting (attempt ${attempt})…`
);
} }
}); });
manager.on('reconnected', () => { manager.on('reconnected', () => {
lastReconnectToastAt = 0;
hideStatusIndicator(); hideStatusIndicator();
if (typeof showAlert === 'function') showAlert('success', 'Reconnected to peardock server'); if (typeof showAlert === 'function') showAlert('success', 'Reconnected to peardock server');
if (hasActiveConnection()) { if (hasActiveConnection()) {
hideWelcomePage(); hideWelcomePage();
hideRestoringPage();
sendCommand(Methods.listContainers); sendCommand(Methods.listContainers);
} }
}); });
manager.on('reconnect-failed', () => { manager.on('reconnect-failed', () => {
// Only fires if a finite attempt cap is configured (default: unlimited)
hideStatusIndicator(); hideStatusIndicator();
if (typeof showAlert === 'function') { if (typeof showAlert === 'function') {
showAlert('danger', 'Could not reconnect after multiple attempts. Re-add the connection.'); showAlert('danger', 'Could not reconnect after multiple attempts. Re-add the connection.');
+25 -6
View File
@@ -14,9 +14,17 @@ import {
} from './peerCache.js' } from './peerCache.js'
import { normalizeRpcError, isBackgroundMethod } from './errors.js' import { normalizeRpcError, isBackgroundMethod } from './errors.js'
const MAX_RECONNECT_ATTEMPTS = 12 /** Fixed interval between reconnect dials (user expectation: keep trying every 5s) */
const BASE_RECONNECT_MS = CONFIG.CONNECTION.RECONNECT_DELAY_MS || 5000 const RECONNECT_DELAY_MS = CONFIG.CONNECTION.RECONNECT_DELAY_MS || 5000
const MAX_RECONNECT_MS = 60_000 /**
* 0 / non-finite = unlimited retries until intentional disconnect or success.
* Positive number = stop after that many failed dials (legacy cap).
*/
const MAX_RECONNECT_ATTEMPTS = (() => {
const n = Number(CONFIG.CONNECTION.RECONNECT_MAX_ATTEMPTS)
if (!Number.isFinite(n) || n <= 0) return Infinity
return n
})()
export class ConnectionManager extends EventEmitter { export class ConnectionManager extends EventEmitter {
constructor() { constructor() {
@@ -197,19 +205,19 @@ export class ConnectionManager extends EventEmitter {
} }
/** /**
* Exponential backoff reconnect after unexpected disconnect. * Reconnect after unexpected disconnect — fixed interval, keeps retrying.
* @param {string} id * @param {string} id
*/ */
_scheduleReconnect(id) { _scheduleReconnect(id) {
const entry = this._reconnect.get(id) const entry = this._reconnect.get(id)
if (!entry || entry.intentional) return if (!entry || entry.intentional) return
if (entry.attempts >= MAX_RECONNECT_ATTEMPTS) { if (Number.isFinite(MAX_RECONNECT_ATTEMPTS) && entry.attempts >= MAX_RECONNECT_ATTEMPTS) {
this.emit('reconnect-failed', { id, publicKeyHex: entry.publicKeyHex, attempts: entry.attempts }) this.emit('reconnect-failed', { id, publicKeyHex: entry.publicKeyHex, attempts: entry.attempts })
return return
} }
this._clearReconnectTimer(id) this._clearReconnectTimer(id)
const delay = Math.min(MAX_RECONNECT_MS, BASE_RECONNECT_MS * 2 ** entry.attempts) const delay = RECONNECT_DELAY_MS
entry.attempts += 1 entry.attempts += 1
this.emit('reconnecting', { this.emit('reconnecting', {
id, id,
@@ -220,12 +228,23 @@ export class ConnectionManager extends EventEmitter {
entry.timer = setTimeout(async () => { entry.timer = setTimeout(async () => {
entry.timer = null entry.timer = null
if (entry.intentional) return
// Already live (e.g. user re-added) — stop the loop
const live = this.connections.get(id)
if (live?.connected) {
entry.attempts = 0
return
}
try { try {
await this.connect(entry.publicKeyHex, { await this.connect(entry.publicKeyHex, {
alias: entry.alias || undefined, alias: entry.alias || undefined,
inviteToken: entry.inviteToken || undefined, inviteToken: entry.inviteToken || undefined,
skipReconnectReset: true, skipReconnectReset: true,
// Restore last-active peer when it comes back; don't steal active on
// background peer reconnect unless nothing is active
setActive: !this.active?.connected,
}) })
entry.attempts = 0
this.emit('reconnected', { id, publicKeyHex: entry.publicKeyHex }) this.emit('reconnected', { id, publicKeyHex: entry.publicKeyHex })
} catch (err) { } catch (err) {
this.emit('reconnect-error', { id, error: err, attempt: entry.attempts }) this.emit('reconnect-error', { id, error: err, attempt: entry.attempts })
+7 -1
View File
@@ -22,7 +22,13 @@ export const CONFIG = {
// Connection // Connection
CONNECTION: { CONNECTION: {
TIMEOUT_MS: 30000, // Connection timeout TIMEOUT_MS: 30000, // Connection timeout
RECONNECT_DELAY_MS: 5000, // Reconnect delay /** Fixed delay between automatic peer reconnect attempts */
RECONNECT_DELAY_MS: 5000,
/**
* Max automatic reconnect attempts (0 / Infinity = never give up).
* Peers keep dialing every RECONNECT_DELAY_MS until back online or removed.
*/
RECONNECT_MAX_ATTEMPTS: 0,
HEALTH_CHECK_INTERVAL_MS: 10000, // Health check interval HEALTH_CHECK_INTERVAL_MS: 10000, // Health check interval
}, },
+2 -1
View File
@@ -1526,9 +1526,10 @@ export function initOpsApp({ navigateToView, sendCommand }) {
} }
}) })
manager.on('reconnecting', ({ attempt, delayMs }) => { manager.on('reconnecting', ({ attempt, delayMs }) => {
const secs = Math.max(1, Math.round((delayMs || 5000) / 1000))
setOfflineBanner( setOfflineBanner(
true, true,
`Connection lost — reconnecting (attempt ${attempt}) in ${Math.round(delayMs / 1000)}s` `Connection lost — retrying every ${secs}s (attempt ${attempt})`
) )
}) })
manager.on('reconnected', () => { manager.on('reconnected', () => {