Async-Redis
view release on metacpan or search on metacpan
examples/pagi-chat/public/js/app.js view on Meta::CPAN
setConnectionStatus(state.reconnectAttempts > 0 ? 'reconnecting' : 'connecting');
state.ws = new WebSocket(wsUrl);
state.ws.onopen = () => {
setConnectionStatus('connected');
state.reconnectAttempts = 0;
state.lastPongTime = Date.now();
// Start keepalive ping interval
if (state.pingInterval) {
clearInterval(state.pingInterval);
}
state.pingInterval = setInterval(() => {
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
sendMessage({ type: 'ping' });
}
}, state.pingIntervalMs);
// Start heartbeat timeout check
if (state.heartbeatCheckInterval) {
clearInterval(state.heartbeatCheckInterval);
}
state.heartbeatCheckInterval = setInterval(() => {
const timeSinceLastPong = Date.now() - state.lastPongTime;
if (timeSinceLastPong > state.heartbeatTimeoutMs) {
console.warn('Heartbeat timeout - connection appears dead, reconnecting...');
if (state.ws) {
state.ws.close();
}
}
}, 5000); // Check every 5 seconds
};
state.ws.onclose = (event) => {
setConnectionStatus('disconnected');
// Clear intervals
if (state.pingInterval) {
clearInterval(state.pingInterval);
state.pingInterval = null;
}
if (state.heartbeatCheckInterval) {
clearInterval(state.heartbeatCheckInterval);
state.heartbeatCheckInterval = null;
}
// Always try to reconnect with exponential backoff
state.reconnectAttempts++;
const delay = calculateReconnectDelay();
console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${state.reconnectAttempts})...`);
setConnectionStatus('reconnecting');
setTimeout(connectWebSocket, delay);
};
state.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
state.ws.onmessage = (event) => {
// Any message resets the heartbeat timer
state.lastPongTime = Date.now();
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (e) {
console.error('Failed to parse message:', e);
}
};
}
function handleWebSocketMessage(data) {
// Track message IDs for catch-up on reconnect
if (data.id && data.id > state.lastMsgId) {
state.lastMsgId = data.id;
}
switch (data.type) {
case 'connected':
// New connection - store session info
state.userId = data.user_id;
state.sessionId = data.session_id;
state.username = data.name;
localStorage.setItem('chat-session-id', data.session_id);
updateUserInfo();
updateRoomsList(data.rooms.map(name => ({ name, users: 0 })));
break;
case 'resumed':
// Session resumed after reconnect
state.userId = data.session_id; // session_id is the user_id
state.sessionId = data.session_id;
state.username = data.name;
localStorage.setItem('chat-session-id', data.session_id);
updateUserInfo();
// Restore room state
state.rooms = {};
data.rooms.forEach(room => state.rooms[room] = true);
// Apply missed messages
if (data.missedMessages) {
for (const [room, messages] of Object.entries(data.missedMessages)) {
if (room === state.currentRoom) {
messages.forEach(msg => {
if (msg.type === 'system') {
addSystemMessage(msg.text, false);
} else {
addMessage(msg, false);
}
// Track highest message ID
if (msg.id && msg.id > state.lastMsgId) {
state.lastMsgId = msg.id;
}
});
scrollToBottom();
}
}
}
( run in 1.494 second using v1.01-cache-2.11-cpan-bbcb1afb8fc )