Aniworld/src/server/web/static/js/websocket_client.js
2025-10-24 20:10:40 +02:00

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;
}