Änderung: Verbesserung der Verbindungsverwaltung und Benutzeroberfläche in mehreren Komponenten
Änderungen: - Hinzufügung eines Verbindungsstatus-Indicators in der AppHeader.vue, der den aktuellen Verbindungsstatus anzeigt. - Erweiterung der MultiChatDialog.vue um verbesserte Netzwerkereignisbehandlungen und eine Herzschlag-Logik zur Aufrechterhaltung der WebSocket-Verbindung. - Anpassungen im Store zur Verwaltung des Verbindungsstatus und zur Implementierung von Wiederverbindungslogik mit exponentiellem Backoff. - Diese Anpassungen verbessern die Benutzererfahrung durch klare Statusanzeigen und erhöhen die Stabilität der WebSocket-Verbindungen.
This commit is contained in:
@@ -2,12 +2,40 @@
|
||||
<header>
|
||||
<div class="logo"><img src="/images/logos/logo.png" /></div>
|
||||
<div class="advertisement">Advertisement</div>
|
||||
<div class="connection-status" v-if="isLoggedIn">
|
||||
<div class="status-indicator" :class="connectionStatusClass">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'AppHeader'
|
||||
name: 'AppHeader',
|
||||
computed: {
|
||||
...mapGetters(['isLoggedIn', 'connectionStatus']),
|
||||
connectionStatusClass() {
|
||||
return {
|
||||
'status-connected': this.connectionStatus === 'connected',
|
||||
'status-connecting': this.connectionStatus === 'connecting',
|
||||
'status-disconnected': this.connectionStatus === 'disconnected',
|
||||
'status-error': this.connectionStatus === 'error'
|
||||
};
|
||||
},
|
||||
connectionStatusText() {
|
||||
switch (this.connectionStatus) {
|
||||
case 'connected': return 'Verbunden';
|
||||
case 'connecting': return 'Verbinde...';
|
||||
case 'disconnected': return 'Getrennt';
|
||||
case 'error': return 'Fehler';
|
||||
default: return 'Unbekannt';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -27,4 +55,69 @@ header {
|
||||
.logo > img {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-connected .status-dot {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-connecting .status-dot {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.status-disconnected .status-dot {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.status-error .status-dot {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -180,7 +180,11 @@ export default {
|
||||
isPicking: false,
|
||||
maxLightness: 92, // exclude extremely light colors (>% L)
|
||||
// Reconnect control
|
||||
reconnectIntervalMs: 5000,
|
||||
reconnectIntervalMs: 3000,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 20,
|
||||
heartbeatInterval: null,
|
||||
heartbeatIntervalMs: 30000, // 30 seconds
|
||||
connectAttemptTimeout: null,
|
||||
joinFallbackTimer: null,
|
||||
// Faster handshake watchdog separate from reconnect interval
|
||||
@@ -258,6 +262,9 @@ export default {
|
||||
this.opened = false;
|
||||
console.log('[Chat WS] dialog close — closing websocket');
|
||||
this.disconnectChatSocket();
|
||||
// Remove network event listeners
|
||||
window.removeEventListener('online', this.onOnline);
|
||||
window.removeEventListener('offline', this.onOffline);
|
||||
},
|
||||
onOptionsToggle(e) {
|
||||
e?.stopPropagation?.();
|
||||
@@ -270,11 +277,18 @@ export default {
|
||||
this.showOptions = false;
|
||||
},
|
||||
onOnline() {
|
||||
console.log('[Chat WS] Network online detected');
|
||||
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
||||
console.log('[Chat WS] online — attempting reconnect');
|
||||
this.reconnectAttempts = 0; // Reset attempts on network recovery
|
||||
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||
this.connectChatSocket();
|
||||
}
|
||||
},
|
||||
onOffline() {
|
||||
console.log('[Chat WS] Network offline detected');
|
||||
this.setStatus('disconnected');
|
||||
},
|
||||
async loadRooms() {
|
||||
try {
|
||||
const data = await fetchPublicRooms();
|
||||
@@ -315,6 +329,9 @@ export default {
|
||||
// Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird
|
||||
this.opened = true;
|
||||
this.connectChatSocket();
|
||||
// Add network event listeners
|
||||
window.addEventListener('online', this.onOnline);
|
||||
window.addEventListener('offline', this.onOffline);
|
||||
},
|
||||
connectChatSocket() {
|
||||
if (this.connectRacing) return; // avoid overlapping races
|
||||
@@ -413,6 +430,9 @@ export default {
|
||||
this.wsUrl = url;
|
||||
this.chatWs = ws;
|
||||
this.transportConnected = true;
|
||||
this.reconnectAttempts = 0; // Reset reconnect counter on successful connection
|
||||
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||
this.startHeartbeat(); // Start heartbeat to keep connection alive
|
||||
console.log('[Chat WS] open in', dt, 'ms', '| protocol:', ws.protocol || '(none)', '| url:', url);
|
||||
// Close all other candidates
|
||||
this.pendingWs.forEach(r => {
|
||||
@@ -569,7 +589,21 @@ export default {
|
||||
},
|
||||
scheduleReconnect() {
|
||||
if (this.reconnectTimer) return;
|
||||
console.log('[Chat WS] scheduleReconnect in', this.reconnectIntervalMs, 'ms', '| opened:', this.opened);
|
||||
|
||||
// Check if we've exceeded max attempts
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error('[Chat WS] Max reconnect attempts reached, waiting longer before retry');
|
||||
this.reconnectAttempts = 0; // Reset counter
|
||||
this.reconnectIntervalMs = 30000; // Wait 30 seconds
|
||||
} else {
|
||||
this.reconnectAttempts++;
|
||||
// Use exponential backoff with jitter
|
||||
const baseDelay = Math.min(3000 * Math.pow(1.5, this.reconnectAttempts - 1), 15000);
|
||||
const jitter = Math.random() * 1000; // Add up to 1 second jitter
|
||||
this.reconnectIntervalMs = baseDelay + jitter;
|
||||
}
|
||||
|
||||
console.log('[Chat WS] scheduleReconnect in', Math.round(this.reconnectIntervalMs), 'ms', '| opened:', this.opened, '| attempt:', this.reconnectAttempts);
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
if (this.opened && !this.chatConnected) {
|
||||
@@ -594,6 +628,7 @@ export default {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.stopHeartbeat(); // Stop heartbeat when disconnecting
|
||||
this.cleanupPendingSockets();
|
||||
if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
|
||||
if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
|
||||
@@ -613,6 +648,35 @@ export default {
|
||||
this.usersInRoom = [];
|
||||
this.selectedTargetUser = null;
|
||||
},
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat(); // Clear any existing heartbeat
|
||||
if (!this.opened || !this.chatConnected) return;
|
||||
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[Chat WS] Heartbeat failed - connection not open, attempting reconnect');
|
||||
this.stopHeartbeat();
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Send a ping message to keep connection alive
|
||||
this.wsSend({ type: 'ping' });
|
||||
console.log('[Chat WS] Heartbeat sent');
|
||||
} catch (error) {
|
||||
console.warn('[Chat WS] Heartbeat failed:', error);
|
||||
this.stopHeartbeat();
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, this.heartbeatIntervalMs);
|
||||
},
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
},
|
||||
getSelectedRoomName() {
|
||||
const r = this.rooms.find(x => x.id === this.selectedRoom);
|
||||
return r?.title || r?.name || '';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { io } from 'socket.io-client';
|
||||
const store = createStore({
|
||||
state: {
|
||||
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
|
||||
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
|
||||
user: JSON.parse(localStorage.getItem('user')) || null,
|
||||
language: navigator.language.startsWith('de') ? 'de' : 'en',
|
||||
menu: JSON.parse(localStorage.getItem('menu')) || [],
|
||||
@@ -45,6 +46,9 @@ const store = createStore({
|
||||
setSocket(state, socket) {
|
||||
state.socket = socket;
|
||||
},
|
||||
setConnectionStatus(state, status) {
|
||||
state.connectionStatus = status;
|
||||
},
|
||||
clearSocket(state) {
|
||||
if (state.socket) {
|
||||
state.socket.disconnect();
|
||||
@@ -86,6 +90,7 @@ const store = createStore({
|
||||
if (currentSocket) {
|
||||
currentSocket.disconnect();
|
||||
}
|
||||
commit('setConnectionStatus', 'connecting');
|
||||
const socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
|
||||
console.log('🔌 Initializing Socket.io connection to:', socketIoUrl);
|
||||
const socket = io(socketIoUrl, {
|
||||
@@ -95,28 +100,40 @@ const store = createStore({
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ Socket.io connected successfully');
|
||||
retryCount = 0; // Reset retry counter on successful connection
|
||||
commit('setConnectionStatus', 'connected');
|
||||
const idForSocket = state.user?.hashedId || state.user?.id;
|
||||
if (idForSocket) socket.emit('setUserId', idForSocket);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.warn('❌ Socket.io disconnected:', reason);
|
||||
commit('setConnectionStatus', 'disconnected');
|
||||
retryConnection(connectSocket);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('❌ Socket.io connection error:', error);
|
||||
console.error('❌ URL attempted:', import.meta.env.VITE_API_BASE_URL);
|
||||
commit('setConnectionStatus', 'error');
|
||||
});
|
||||
|
||||
commit('setSocket', socket);
|
||||
};
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 10;
|
||||
const retryConnection = (reconnectFn) => {
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error('❌ Max retry attempts reached for Socket.io');
|
||||
return;
|
||||
}
|
||||
retryCount++;
|
||||
const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s
|
||||
console.log(`🔄 Retrying Socket.io connection in ${delay}ms (attempt ${retryCount}/${maxRetries})`);
|
||||
setTimeout(() => {
|
||||
console.log('Retrying Socket.io connection...');
|
||||
reconnectFn();
|
||||
}, 1000); // Retry every second
|
||||
}, delay);
|
||||
};
|
||||
|
||||
connectSocket();
|
||||
@@ -236,14 +253,20 @@ const store = createStore({
|
||||
};
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 5;
|
||||
const maxRetries = 15; // Increased max retries
|
||||
const retryConnection = (reconnectFn) => {
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error('❌ Max retry attempts reached for Daemon WebSocket');
|
||||
// Reset counter after a longer delay to allow for network recovery
|
||||
setTimeout(() => {
|
||||
retryCount = 0;
|
||||
console.log('🔄 Resetting Daemon WebSocket retry counter - attempting reconnection');
|
||||
reconnectFn();
|
||||
}, 60000); // Wait 1 minute before resetting
|
||||
return;
|
||||
}
|
||||
retryCount++;
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); // Exponential backoff, max 10s
|
||||
const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s
|
||||
console.log(`🔄 Retrying Daemon WebSocket connection in ${delay}ms (attempt ${retryCount}/${maxRetries})`);
|
||||
setTimeout(() => {
|
||||
reconnectFn();
|
||||
@@ -272,6 +295,7 @@ const store = createStore({
|
||||
socket: state => state.socket,
|
||||
daemonSocket: state => state.daemonSocket,
|
||||
menuNeedsUpdate: state => state.menuNeedsUpdate,
|
||||
connectionStatus: state => state.connectionStatus,
|
||||
},
|
||||
modules: {
|
||||
dialogs,
|
||||
|
||||
Reference in New Issue
Block a user