mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Add Socket.IO integration with useSocket hook and socketService
- Introduced a new useSocket hook for managing Socket.IO connections in React components, providing methods for emitting and listening to events. - Created a socketService class to handle the Socket.IO connection, including automatic reconnection and event management. - Implemented token-based authentication for socket connections using the Zustand auth store, ensuring secure communication with the server. - Added event handlers for connection status and error management to enhance reliability. These changes enable real-time communication capabilities in the application, improving user interaction and responsiveness.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { socketService } from '../services/socketService';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
|
||||
interface UseSocketOptions {
|
||||
autoConnect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for using the Socket.IO connection
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { socket, isConnected, emit, on, off } = useSocket();
|
||||
*
|
||||
* useEffect(() => {
|
||||
* on('ready', () => {
|
||||
* console.log('Socket ready!');
|
||||
* });
|
||||
*
|
||||
* return () => {
|
||||
* off('ready');
|
||||
* };
|
||||
* }, [on, off]);
|
||||
* ```
|
||||
*/
|
||||
export const useSocket = (options: UseSocketOptions = {}) => {
|
||||
const { autoConnect = true } = options;
|
||||
const listenersRef = useRef<Array<{ event: string; callback: (...args: unknown[]) => void }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoConnect) {
|
||||
socketService.connect();
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup: remove all listeners registered through this hook
|
||||
listenersRef.current.forEach(({ event, callback }) => {
|
||||
socketService.off(event, callback);
|
||||
});
|
||||
listenersRef.current = [];
|
||||
};
|
||||
}, [autoConnect]);
|
||||
|
||||
const emit = (event: string, data?: unknown) => {
|
||||
socketService.emit(event, data);
|
||||
};
|
||||
|
||||
const on = (event: string, callback: (...args: unknown[]) => void) => {
|
||||
socketService.on(event, callback);
|
||||
listenersRef.current.push({ event, callback });
|
||||
};
|
||||
|
||||
const off = (event: string, callback?: (...args: unknown[]) => void) => {
|
||||
socketService.off(event, callback);
|
||||
if (callback) {
|
||||
listenersRef.current = listenersRef.current.filter(
|
||||
(listener) => listener.event !== event || listener.callback !== callback
|
||||
);
|
||||
} else {
|
||||
listenersRef.current = listenersRef.current.filter((listener) => listener.event !== event);
|
||||
}
|
||||
};
|
||||
|
||||
const once = (event: string, callback: (...args: unknown[]) => void) => {
|
||||
socketService.once(event, callback);
|
||||
};
|
||||
|
||||
return {
|
||||
socket: socketService.getSocket() as Socket | null,
|
||||
isConnected: socketService.isConnected(),
|
||||
emit,
|
||||
on,
|
||||
off,
|
||||
once,
|
||||
connect: () => socketService.connect(),
|
||||
disconnect: () => socketService.disconnect(),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { BACKEND_URL } from '../utils/config';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
class SocketService {
|
||||
private socket: Socket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000; // Start with 1 second
|
||||
|
||||
/**
|
||||
* Connect to the Socket.IO server using the JWT token from the auth store
|
||||
*/
|
||||
connect(): void {
|
||||
const token = useAuthStore.getState().token;
|
||||
|
||||
if (!token) {
|
||||
console.warn('[SocketService] No token available, cannot connect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect existing connection if any
|
||||
if (this.socket?.connected) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
console.log('[SocketService] Connecting to socket server...');
|
||||
|
||||
this.socket = io(BACKEND_URL, {
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
path: '/socket.io/',
|
||||
transports: ['polling', 'websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: this.maxReconnectAttempts,
|
||||
reconnectionDelay: this.reconnectDelay,
|
||||
reconnectionDelayMax: 5000,
|
||||
});
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the Socket.IO server
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.socket) {
|
||||
console.log('[SocketService] Disconnecting from socket server...');
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.reconnectAttempts = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the socket is connected
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.socket?.connected ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the socket instance (use with caution)
|
||||
*/
|
||||
getSocket(): Socket | null {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to the server
|
||||
*/
|
||||
emit(event: string, data?: unknown): void {
|
||||
if (!this.socket?.connected) {
|
||||
console.warn(`[SocketService] Cannot emit '${event}': socket not connected`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.emit(event, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an event from the server
|
||||
*/
|
||||
on(event: string, callback: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener
|
||||
*/
|
||||
off(event: string, callback?: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.off(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an event once
|
||||
*/
|
||||
once(event: string, callback: (...args: unknown[]) => void): void {
|
||||
if (!this.socket) {
|
||||
console.warn(`[SocketService] Cannot listen to '${event}': socket not initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.once(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for connection, disconnection, and errors
|
||||
*/
|
||||
private setupEventHandlers(): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('[SocketService] Connected to socket server:', this.socket?.id);
|
||||
this.reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
console.log('[SocketService] Disconnected from socket server:', reason);
|
||||
});
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('[SocketService] Connection error:', error.message);
|
||||
this.reconnectAttempts++;
|
||||
|
||||
// If max attempts reached, try reconnecting with fresh token
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('[SocketService] Max reconnection attempts reached, reconnecting with fresh token...');
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay * 2);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('ready', () => {
|
||||
console.log('[SocketService] Server ready');
|
||||
});
|
||||
|
||||
this.socket.on('error', (error: { message?: string; status?: number; requestId?: string }) => {
|
||||
console.error('[SocketService] Server error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const socketService = new SocketService();
|
||||
|
||||
// Auto-connect when token is available
|
||||
// Listen to auth store changes
|
||||
if (typeof window !== 'undefined') {
|
||||
let previousToken: string | null = null;
|
||||
|
||||
const checkAndUpdateConnection = () => {
|
||||
const currentToken = useAuthStore.getState().token;
|
||||
|
||||
// Only update connection if token actually changed
|
||||
if (currentToken !== previousToken) {
|
||||
previousToken = currentToken;
|
||||
|
||||
if (currentToken && !socketService.isConnected()) {
|
||||
socketService.connect();
|
||||
} else if (!currentToken && socketService.isConnected()) {
|
||||
socketService.disconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial connection check
|
||||
checkAndUpdateConnection();
|
||||
|
||||
// Subscribe to auth store changes
|
||||
useAuthStore.subscribe((state) => {
|
||||
const currentToken = state.token;
|
||||
|
||||
if (currentToken !== previousToken) {
|
||||
previousToken = currentToken;
|
||||
|
||||
if (currentToken && !socketService.isConnected()) {
|
||||
socketService.connect();
|
||||
} else if (!currentToken && socketService.isConnected()) {
|
||||
socketService.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user