233 lines
6.2 KiB
JavaScript
233 lines
6.2 KiB
JavaScript
/**
|
|
* Native WebSocket Client Wrapper
|
|
* Provides Socket.IO-like interface using native WebSocket API
|
|
*
|
|
* This wrapper maintains compatibility with existing Socket.IO-style
|
|
* event handlers while using the modern WebSocket API underneath.
|
|
*/
|
|
|
|
class WebSocketClient {
|
|
constructor(url = null) {
|
|
this.ws = null;
|
|
this.url = url || this.getWebSocketUrl();
|
|
this.eventHandlers = new Map();
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = 5;
|
|
this.reconnectDelay = 1000;
|
|
this.isConnected = false;
|
|
this.rooms = new Set();
|
|
this.messageQueue = [];
|
|
this.autoReconnect = true;
|
|
}
|
|
|
|
/**
|
|
* Get WebSocket URL based on current page URL
|
|
*/
|
|
getWebSocketUrl() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const host = window.location.host;
|
|
return `${protocol}//${host}/ws/connect`;
|
|
}
|
|
|
|
/**
|
|
* Connect to WebSocket server
|
|
*/
|
|
connect() {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
console.log('WebSocket already connected');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.ws = new WebSocket(this.url);
|
|
|
|
this.ws.onopen = () => {
|
|
console.log('WebSocket connected');
|
|
this.isConnected = true;
|
|
this.reconnectAttempts = 0;
|
|
|
|
// Emit connect event
|
|
this.emit('connect');
|
|
|
|
// Rejoin rooms
|
|
this.rejoinRooms();
|
|
|
|
// Process queued messages
|
|
this.processMessageQueue();
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
this.handleMessage(event.data);
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
this.emit('error', { error: 'WebSocket connection error' });
|
|
};
|
|
|
|
this.ws.onclose = (event) => {
|
|
console.log('WebSocket disconnected', event.code, event.reason);
|
|
this.isConnected = false;
|
|
this.emit('disconnect', { code: event.code, reason: event.reason });
|
|
|
|
// Attempt reconnection
|
|
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
this.reconnectAttempts++;
|
|
const delay = this.reconnectDelay * this.reconnectAttempts;
|
|
console.log(`Attempting reconnection in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
setTimeout(() => this.connect(), delay);
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to create WebSocket connection:', error);
|
|
this.emit('error', { error: 'Failed to connect' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect from WebSocket server
|
|
*/
|
|
disconnect() {
|
|
this.autoReconnect = false;
|
|
if (this.ws) {
|
|
this.ws.close(1000, 'Client disconnected');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle incoming WebSocket message
|
|
*/
|
|
handleMessage(data) {
|
|
try {
|
|
const message = JSON.parse(data);
|
|
const { type, data: payload, timestamp } = message;
|
|
|
|
// Emit event with payload
|
|
if (type) {
|
|
this.emit(type, payload || {});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to parse WebSocket message:', error, data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register event handler (Socket.IO-style)
|
|
*/
|
|
on(event, handler) {
|
|
if (!this.eventHandlers.has(event)) {
|
|
this.eventHandlers.set(event, []);
|
|
}
|
|
this.eventHandlers.get(event).push(handler);
|
|
}
|
|
|
|
/**
|
|
* Remove event handler
|
|
*/
|
|
off(event, handler) {
|
|
if (!this.eventHandlers.has(event)) {
|
|
return;
|
|
}
|
|
|
|
const handlers = this.eventHandlers.get(event);
|
|
const index = handlers.indexOf(handler);
|
|
if (index !== -1) {
|
|
handlers.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit event to registered handlers
|
|
*/
|
|
emit(event, data = null) {
|
|
if (!this.eventHandlers.has(event)) {
|
|
return;
|
|
}
|
|
|
|
const handlers = this.eventHandlers.get(event);
|
|
handlers.forEach(handler => {
|
|
try {
|
|
if (data !== null) {
|
|
handler(data);
|
|
} else {
|
|
handler();
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error in event handler for ${event}:`, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send message to server
|
|
*/
|
|
send(action, data = {}) {
|
|
const message = JSON.stringify({
|
|
action,
|
|
data
|
|
});
|
|
|
|
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(message);
|
|
} else {
|
|
console.warn('WebSocket not connected, queueing message');
|
|
this.messageQueue.push(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Join a room (subscribe to topic)
|
|
*/
|
|
join(room) {
|
|
this.rooms.add(room);
|
|
if (this.isConnected) {
|
|
this.send('join', { room });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Leave a room (unsubscribe from topic)
|
|
*/
|
|
leave(room) {
|
|
this.rooms.delete(room);
|
|
if (this.isConnected) {
|
|
this.send('leave', { room });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rejoin all rooms after reconnection
|
|
*/
|
|
rejoinRooms() {
|
|
this.rooms.forEach(room => {
|
|
this.send('join', { room });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process queued messages after connection
|
|
*/
|
|
processMessageQueue() {
|
|
while (this.messageQueue.length > 0 && this.isConnected) {
|
|
const message = this.messageQueue.shift();
|
|
this.ws.send(message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if connected
|
|
*/
|
|
connected() {
|
|
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create global io() function for Socket.IO compatibility
|
|
*/
|
|
function io(url = null) {
|
|
const client = new WebSocketClient(url);
|
|
client.connect();
|
|
return client;
|
|
}
|