feat: add WebSocket client with fallback

- Add Socket.IO client library (v4.7.5)
- Create RFMapperWS class with:
  - Automatic reconnection (5 attempts)
  - HTTP polling fallback
  - Event listener system for scanUpdate, connected, disconnected
  - Floor subscription support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
User
2026-02-01 04:55:55 +01:00
parent 14757f2e57
commit 8f4fa4e186
2 changed files with 108 additions and 0 deletions

7
src/rf_mapper/web/static/js/vendor/socket.io.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,101 @@
/**
* RF Mapper WebSocket client with automatic reconnection and HTTP fallback
*/
class RFMapperWS {
constructor() {
this.socket = null;
this.connected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.listeners = {
scanUpdate: [],
connected: [],
disconnected: []
};
}
connect() {
// Check if socket.io is loaded
if (typeof io === 'undefined') {
console.warn('[WS] socket.io not loaded, using HTTP polling');
return false;
}
try {
this.socket = io('/ws/scan', {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: this.maxReconnectAttempts
});
this.socket.on('connect', () => {
console.log('[WS] Connected');
this.connected = true;
this.reconnectAttempts = 0;
this._emit('connected');
});
this.socket.on('disconnect', (reason) => {
console.log('[WS] Disconnected:', reason);
this.connected = false;
this._emit('disconnected', { reason });
});
this.socket.on('scan_update', (data) => {
this._emit('scanUpdate', data);
});
this.socket.on('connect_error', (error) => {
console.warn('[WS] Connection error:', error.message);
this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('[WS] Max reconnect attempts, falling back to HTTP');
this.connected = false;
this._emit('disconnected', { reason: 'max_reconnect' });
}
});
return true;
} catch (e) {
console.error('[WS] Failed to initialize:', e);
return false;
}
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.connected = false;
}
subscribeFloor(floor) {
if (this.socket?.connected) {
this.socket.emit('subscribe_floor', { floor });
}
}
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
}
_emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(cb => cb(data));
}
}
}
// Global instance
const rfMapperWS = new RFMapperWS();