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:
@@ -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
@@ -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 })
|
||||||
|
|||||||
@@ -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
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user