![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/context/ |
import React, { createContext, useContext, useEffect, useRef, useState, ReactNode, useCallback } from 'react';
import { useSession } from 'next-auth/react';
// Enhanced Types
interface WebSocketMessage {
id: string;
type: string;
data: any;
timestamp: number;
requiresAck?: boolean;
}
interface PendingMessage {
message: WebSocketMessage;
resolve: (value: any) => void;
reject: (error: Error) => void;
retryCount: number;
timeout: NodeJS.Timeout;
}
interface UserPresence {
userId: string;
status: 'online' | 'away' | 'offline';
lastSeen: number;
currentRoom?: string;
}
interface TypingState {
roomId: string;
userId: string;
userName: string;
timestamp: number;
}
interface WebSocketContextValue {
ws: WebSocket | null;
connected: boolean;
connectionState: 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
// Message handling
sendMessage: (type: string, data: any, requiresAck?: boolean) => Promise<any>;
sendReliableMessage: (type: string, data: any) => Promise<any>;
// Presence
userPresence: Map<string, UserPresence>;
setUserStatus: (status: 'online' | 'away') => void;
// Typing indicators
typingUsers: Map<string, TypingState[]>;
sendTyping: (roomId: string, isTyping: boolean) => void;
// Room management
joinRoom: (roomId: string) => Promise<void>;
leaveRoom: (roomId: string) => Promise<void>;
// Case chat management
joinCaseChat: (caseId: string) => Promise<void>;
leaveCaseChat: (caseId: string) => Promise<void>;
sendCaseTyping: (caseId: string, isTyping: boolean) => void;
// Connection stats
connectionStats: {
reconnectAttempts: number;
lastConnected: number | null;
messagesSent: number;
messagesReceived: number;
latency: number | null;
};
}
const WebSocketContext = createContext<WebSocketContextValue>({
ws: null,
connected: false,
connectionState: 'disconnected',
sendMessage: async () => {},
sendReliableMessage: async () => {},
userPresence: new Map(),
setUserStatus: () => {},
typingUsers: new Map(),
sendTyping: () => {},
joinRoom: async () => {},
leaveRoom: async () => {},
joinCaseChat: async () => {},
leaveCaseChat: async () => {},
sendCaseTyping: () => {},
connectionStats: {
reconnectAttempts: 0,
lastConnected: null,
messagesSent: 0,
messagesReceived: 0,
latency: null,
},
});
export const WebSocketProvider = ({ children }: { children: ReactNode }) => {
const { data: session, status } = useSession();
// WebSocket connection
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [connected, setConnected] = useState(false);
const [connectionState, setConnectionState] = useState<'disconnected' | 'connecting' | 'connected' | 'reconnecting'>('disconnected');
const [wsInstance, setWsInstance] = useState<WebSocket | null>(null);
// Message handling
const pendingMessages = useRef<Map<string, PendingMessage>>(new Map());
const messageQueue = useRef<WebSocketMessage[]>([]);
// Presence and typing
const [userPresence, setUserPresence] = useState<Map<string, UserPresence>>(new Map());
const [typingUsers, setTypingUsers] = useState<Map<string, TypingState[]>>(new Map());
const typingTimeouts = useRef<Map<string, NodeJS.Timeout>>(new Map());
// Connection stats
const [connectionStats, setConnectionStats] = useState({
reconnectAttempts: 0,
lastConnected: null as number | null,
messagesSent: 0,
messagesReceived: 0,
latency: null as number | null,
});
// Ping/Pong for latency measurement
const lastPingTime = useRef<number | null>(null);
const pingInterval = useRef<NodeJS.Timeout | null>(null);
// Generate unique message ID
const generateMessageId = useCallback(() => {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}, []);
// Enhanced message sending with acknowledgments
const sendMessage = useCallback(async (type: string, data: any, requiresAck: boolean = false): Promise<any> => {
return new Promise((resolve, reject) => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
const message: WebSocketMessage = {
id: generateMessageId(),
type,
data,
timestamp: Date.now(),
requiresAck,
};
// Queue message for when connection is restored
messageQueue.current.push(message);
if (requiresAck) {
reject(new Error('WebSocket not connected'));
} else {
resolve(null);
}
return;
}
const message: WebSocketMessage = {
id: generateMessageId(),
type,
data,
timestamp: Date.now(),
requiresAck,
};
try {
wsRef.current.send(JSON.stringify(message));
setConnectionStats(prev => ({
...prev,
messagesSent: prev.messagesSent + 1,
}));
if (requiresAck) {
// Set up acknowledgment handling
const timeout = setTimeout(() => {
const pending = pendingMessages.current.get(message.id);
if (pending) {
pendingMessages.current.delete(message.id);
pending.reject(new Error('Message acknowledgment timeout'));
}
}, 10000); // 10 second timeout
pendingMessages.current.set(message.id, {
message,
resolve,
reject,
retryCount: 0,
timeout,
});
} else {
resolve(null);
}
} catch (error) {
reject(error);
}
});
}, [generateMessageId]);
// Reliable message sending with retries
const sendReliableMessage = useCallback(async (type: string, data: any): Promise<any> => {
return sendMessage(type, data, true);
}, [sendMessage]);
// Presence management
const setUserStatus = useCallback((status: 'online' | 'away') => {
if (session?.user?.id) {
sendMessage('PRESENCE_UPDATE', {
userId: session.user.id,
status,
timestamp: Date.now(),
});
}
}, [sendMessage]);
// Typing indicators
const sendTyping = useCallback((roomId: string, isTyping: boolean) => {
if (!session?.user?.id) return;
const key = `${roomId}_${session.user.id}`;
// Clear existing timeout
if (typingTimeouts.current.has(key)) {
clearTimeout(typingTimeouts.current.get(key)!);
typingTimeouts.current.delete(key);
}
sendMessage('TYPING', {
roomId,
userId: session.user.id,
userName: session.user.name,
isTyping,
timestamp: Date.now(),
});
if (isTyping) {
// Auto-stop typing after 3 seconds
const timeout = setTimeout(() => {
sendMessage('TYPING', {
roomId,
userId: session.user.id,
userName: session.user.name,
isTyping: false,
timestamp: Date.now(),
});
}, 3000);
typingTimeouts.current.set(key, timeout);
}
}, [sendMessage]);
// Room management
const joinRoom = useCallback(async (roomId: string): Promise<void> => {
return sendReliableMessage('JOIN_ROOM', { chatRoomId: roomId });
}, [sendReliableMessage]);
const leaveRoom = useCallback(async (roomId: string): Promise<void> => {
return sendReliableMessage('LEAVE_ROOM', { chatRoomId: roomId });
}, [sendReliableMessage]);
// Case chat management
const joinCaseChat = useCallback(async (caseId: string): Promise<void> => {
return sendReliableMessage('JOIN_CASE_CHAT', { caseId });
}, [sendReliableMessage]);
const leaveCaseChat = useCallback(async (caseId: string): Promise<void> => {
return sendReliableMessage('LEAVE_CASE_CHAT', { caseId });
}, [sendReliableMessage]);
const sendCaseTyping = useCallback((caseId: string, isTyping: boolean) => {
if (!session?.user?.id) return;
const key = `case_${caseId}_${session.user.id}`;
// Clear existing timeout
if (typingTimeouts.current.has(key)) {
clearTimeout(typingTimeouts.current.get(key)!);
typingTimeouts.current.delete(key);
}
sendMessage('CASE_TYPING', {
caseId,
userId: session.user.id,
userName: session.user.name,
isTyping,
timestamp: Date.now(),
});
if (isTyping) {
// Auto-stop typing after 3 seconds
const timeout = setTimeout(() => {
sendMessage('CASE_TYPING', {
caseId,
userId: session.user.id,
userName: session.user.name,
isTyping: false,
timestamp: Date.now(),
});
}, 3000);
typingTimeouts.current.set(key, timeout);
}
}, [sendMessage]);
// Process queued messages when connection is restored
const processMessageQueue = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN && messageQueue.current.length > 0) {
console.log(`[WebSocket] 📤 Processing ${messageQueue.current.length} queued messages`);
const messages = [...messageQueue.current];
messageQueue.current = [];
messages.forEach(message => {
try {
wsRef.current!.send(JSON.stringify(message));
setConnectionStats(prev => ({
...prev,
messagesSent: prev.messagesSent + 1,
}));
} catch (error) {
console.error('[WebSocket] ❌ Failed to send queued message:', error);
// Re-queue the message
messageQueue.current.push(message);
}
});
}
}, []);
// Handle incoming messages
const handleMessage = useCallback((event: MessageEvent) => {
try {
const message = JSON.parse(event.data);
setConnectionStats(prev => ({
...prev,
messagesReceived: prev.messagesReceived + 1,
}));
// Handle different message types
switch (message.type) {
case 'pong':
if (lastPingTime.current) {
const latency = Date.now() - lastPingTime.current;
setConnectionStats(prev => ({ ...prev, latency }));
lastPingTime.current = null;
}
break;
case 'ping':
// Respond to server ping
wsRef.current?.send(JSON.stringify({ type: 'pong' }));
break;
case 'MESSAGE_ACK':
// Handle message acknowledgment
const pending = pendingMessages.current.get(message.data.messageId);
if (pending) {
clearTimeout(pending.timeout);
pendingMessages.current.delete(message.data.messageId);
pending.resolve(message.data);
}
break;
case 'PRESENCE_UPDATE':
setUserPresence(prev => {
const newMap = new Map(prev);
newMap.set(message.data.userId, {
userId: message.data.userId,
status: message.data.status,
lastSeen: message.data.timestamp,
currentRoom: message.data.currentRoom,
});
return newMap;
});
break;
case 'TYPING':
setTypingUsers(prev => {
const newMap = new Map(prev);
const roomTyping = newMap.get(message.data.roomId) || [];
if (message.data.isTyping) {
// Add or update typing user
const existingIndex = roomTyping.findIndex(t => t.userId === message.data.userId);
const typingState: TypingState = {
roomId: message.data.roomId,
userId: message.data.userId,
userName: message.data.userName,
timestamp: message.data.timestamp,
};
if (existingIndex >= 0) {
roomTyping[existingIndex] = typingState;
} else {
roomTyping.push(typingState);
}
} else {
// Remove typing user
const filteredTyping = roomTyping.filter(t => t.userId !== message.data.userId);
newMap.set(message.data.roomId, filteredTyping);
return newMap;
}
newMap.set(message.data.roomId, roomTyping);
return newMap;
});
break;
case 'CASE_TYPING':
setTypingUsers(prev => {
const newMap = new Map(prev);
const caseKey = `case_${message.data.caseId}`;
const caseTyping = newMap.get(caseKey) || [];
if (message.data.isTyping) {
// Add or update typing user
const existingIndex = caseTyping.findIndex(t => t.userId === message.data.userId);
const typingState: TypingState = {
roomId: caseKey,
userId: message.data.userId,
userName: message.data.userName,
timestamp: message.data.timestamp,
};
if (existingIndex >= 0) {
caseTyping[existingIndex] = typingState;
} else {
caseTyping.push(typingState);
}
} else {
// Remove typing user
const filteredTyping = caseTyping.filter(t => t.userId !== message.data.userId);
newMap.set(caseKey, filteredTyping);
return newMap;
}
newMap.set(caseKey, caseTyping);
return newMap;
});
break;
default:
// Let other components handle the message via custom events
window.dispatchEvent(new CustomEvent('websocket-message', { detail: message }));
break;
}
} catch (error) {
console.error('[WebSocket] ❌ Failed to parse message:', error);
}
}, []);
// Enhanced connection logic with exponential backoff
const connect = useCallback(() => {
if (!session?.user?.id || status !== 'authenticated') return;
// If already connected or connecting, don't create new connection
if (wsRef.current && wsRef.current.readyState < 2) return;
setConnectionState('connecting');
const userId = session.user.id;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const userPayload = encodeURIComponent(JSON.stringify({
...session.user,
id: userId,
}));
const wsUrl = `${protocol}//${window.location.host}/_ws?userId=${userId}&user=${userPayload}`;
console.log(`[WebSocket] 🔄 Connecting for user: ${session.user.name}`);
try {
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => {
console.log(`[WebSocket] ✅ Connected for ${session.user.name}`);
setConnected(true);
setConnectionState('connected');
setWsInstance(ws);
setConnectionStats(prev => ({
...prev,
reconnectAttempts: 0,
lastConnected: Date.now(),
}));
// Clear any reconnect timeout
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
// Process any queued messages
processMessageQueue();
// Start ping interval for latency measurement
if (pingInterval.current) clearInterval(pingInterval.current);
pingInterval.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
lastPingTime.current = Date.now();
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
// Update presence to online
setUserStatus('online');
};
ws.onmessage = handleMessage;
ws.onclose = (event) => {
console.log(`[WebSocket] 🔌 Disconnected (code: ${event.code}, reason: ${event.reason})`);
if (wsRef.current === ws) {
setConnected(false);
setConnectionState('disconnected');
setWsInstance(null);
wsRef.current = null;
}
// Clear ping interval
if (pingInterval.current) {
clearInterval(pingInterval.current);
pingInterval.current = null;
}
// Don't reconnect if it was a clean close or user logged out
if (event.code === 1000 || event.code === 1001 || status !== 'authenticated') {
return;
}
// Exponential backoff reconnection
if (status === 'authenticated' && session?.user?.id) {
setConnectionState('reconnecting');
const attempts = connectionStats.reconnectAttempts;
const delay = Math.min(1000 * Math.pow(2, attempts), 30000); // Max 30 seconds
console.log(`[WebSocket] 🔄 Reconnecting in ${delay}ms (attempt ${attempts + 1})`);
setConnectionStats(prev => ({
...prev,
reconnectAttempts: prev.reconnectAttempts + 1,
}));
reconnectTimeoutRef.current = setTimeout(connect, delay);
}
};
ws.onerror = (error) => {
console.error('[WebSocket] ❌ Connection error:', error);
};
} catch (error) {
console.error('[WebSocket] ❌ Failed to create connection:', error);
setConnectionState('disconnected');
}
}, [status, connectionStats.reconnectAttempts, handleMessage, processMessageQueue, setUserStatus]);
// Main connection effect
useEffect(() => {
if (status === 'authenticated' && session?.user?.id) {
connect();
} else {
// Clean up when user logs out
if (wsRef.current) {
wsRef.current.close(1000, 'User logged out');
wsRef.current = null;
}
setConnected(false);
setConnectionState('disconnected');
setWsInstance(null);
}
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (pingInterval.current) {
clearInterval(pingInterval.current);
}
if (wsRef.current) {
wsRef.current.close(1000, 'Component unmounting');
wsRef.current = null;
}
};
}, [status, session?.user?.id, connect]);
// Handle page visibility changes
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
setUserStatus('away');
} else {
setUserStatus('online');
// Ensure connection is active when page becomes visible
if (status === 'authenticated' && (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN)) {
connect();
}
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [status, connect, setUserStatus]);
// Clean up typing timeouts on unmount
useEffect(() => {
return () => {
typingTimeouts.current.forEach(timeout => clearTimeout(timeout));
typingTimeouts.current.clear();
};
}, []);
// Cleanup function to prevent memory leaks
const cleanup = useCallback(() => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (pingInterval.current) {
clearInterval(pingInterval.current);
pingInterval.current = null;
}
// Clear all pending messages
pendingMessages.current.forEach(pending => {
clearTimeout(pending.timeout);
});
pendingMessages.current.clear();
// Clear typing timeouts
typingTimeouts.current.forEach(timeout => {
clearTimeout(timeout);
});
typingTimeouts.current.clear();
setConnected(false);
setConnectionState('disconnected');
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
const contextValue: WebSocketContextValue = {
ws: wsInstance,
connected,
connectionState,
sendMessage,
sendReliableMessage,
userPresence,
setUserStatus,
typingUsers,
sendTyping,
joinRoom,
leaveRoom,
joinCaseChat,
leaveCaseChat,
sendCaseTyping,
connectionStats,
};
return (
<WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
};
export const useWebSocket = () => useContext(WebSocketContext);