Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
139
ui/services/api.service.js
Normal file
139
ui/services/api.service.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// API Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildApiUrl } from '../config/api.config.js';
|
||||
|
||||
export class ApiService {
|
||||
constructor() {
|
||||
this.authToken = null;
|
||||
this.requestInterceptors = [];
|
||||
this.responseInterceptors = [];
|
||||
}
|
||||
|
||||
// Set authentication token
|
||||
setAuthToken(token) {
|
||||
this.authToken = token;
|
||||
}
|
||||
|
||||
// Add request interceptor
|
||||
addRequestInterceptor(interceptor) {
|
||||
this.requestInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Add response interceptor
|
||||
addResponseInterceptor(interceptor) {
|
||||
this.responseInterceptors.push(interceptor);
|
||||
}
|
||||
|
||||
// Build headers for requests
|
||||
getHeaders(customHeaders = {}) {
|
||||
const headers = {
|
||||
...API_CONFIG.DEFAULT_HEADERS,
|
||||
...customHeaders
|
||||
};
|
||||
|
||||
if (this.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Process request through interceptors
|
||||
async processRequest(url, options) {
|
||||
let processedUrl = url;
|
||||
let processedOptions = options;
|
||||
|
||||
for (const interceptor of this.requestInterceptors) {
|
||||
const result = await interceptor(processedUrl, processedOptions);
|
||||
processedUrl = result.url || processedUrl;
|
||||
processedOptions = result.options || processedOptions;
|
||||
}
|
||||
|
||||
return { url: processedUrl, options: processedOptions };
|
||||
}
|
||||
|
||||
// Process response through interceptors
|
||||
async processResponse(response, url) {
|
||||
let processedResponse = response;
|
||||
|
||||
for (const interceptor of this.responseInterceptors) {
|
||||
processedResponse = await interceptor(processedResponse, url);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
}
|
||||
|
||||
// Generic request method
|
||||
async request(url, options = {}) {
|
||||
try {
|
||||
// Process request through interceptors
|
||||
const processed = await this.processRequest(url, options);
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(processed.url, {
|
||||
...processed.options,
|
||||
headers: this.getHeaders(processed.options.headers)
|
||||
});
|
||||
|
||||
// Process response through interceptors
|
||||
const processedResponse = await this.processResponse(response, url);
|
||||
|
||||
// Handle errors
|
||||
if (!processedResponse.ok) {
|
||||
const error = await processedResponse.json().catch(() => ({
|
||||
message: `HTTP ${processedResponse.status}: ${processedResponse.statusText}`
|
||||
}));
|
||||
throw new Error(error.message || error.detail || 'Request failed');
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
const data = await processedResponse.json().catch(() => null);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API Request Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// GET request
|
||||
async get(endpoint, params = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint, params);
|
||||
return this.request(url, {
|
||||
method: 'GET',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// POST request
|
||||
async post(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// PUT request
|
||||
async put(endpoint, data = {}, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE request
|
||||
async delete(endpoint, options = {}) {
|
||||
const url = buildApiUrl(endpoint);
|
||||
return this.request(url, {
|
||||
method: 'DELETE',
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const apiService = new ApiService();
|
||||
138
ui/services/health.service.js
Normal file
138
ui/services/health.service.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// Health Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class HealthService {
|
||||
constructor() {
|
||||
this.healthCheckInterval = null;
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
|
||||
// Get system health
|
||||
async getSystemHealth() {
|
||||
const health = await apiService.get(API_CONFIG.ENDPOINTS.HEALTH.SYSTEM);
|
||||
this.lastHealthStatus = health;
|
||||
this.notifySubscribers(health);
|
||||
return health;
|
||||
}
|
||||
|
||||
// Check readiness
|
||||
async checkReadiness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.READY);
|
||||
}
|
||||
|
||||
// Check liveness
|
||||
async checkLiveness() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.LIVE);
|
||||
}
|
||||
|
||||
// Get system metrics
|
||||
async getSystemMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.METRICS);
|
||||
}
|
||||
|
||||
// Get version info
|
||||
async getVersion() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.VERSION);
|
||||
}
|
||||
|
||||
// Get API info
|
||||
async getApiInfo() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.INFO);
|
||||
}
|
||||
|
||||
// Get API status
|
||||
async getApiStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STATUS);
|
||||
}
|
||||
|
||||
// Start periodic health checks
|
||||
startHealthMonitoring(intervalMs = 30000) {
|
||||
if (this.healthCheckInterval) {
|
||||
console.warn('Health monitoring already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial check
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Initial health check failed:', error);
|
||||
});
|
||||
|
||||
// Set up periodic checks
|
||||
this.healthCheckInterval = setInterval(() => {
|
||||
this.getSystemHealth().catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.notifySubscribers({
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
// Stop health monitoring
|
||||
stopHealthMonitoring() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to health updates
|
||||
subscribeToHealth(callback) {
|
||||
this.healthSubscribers.push(callback);
|
||||
|
||||
// Send last known status if available
|
||||
if (this.lastHealthStatus) {
|
||||
callback(this.lastHealthStatus);
|
||||
}
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.healthSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.healthSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(health) {
|
||||
this.healthSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(health);
|
||||
} catch (error) {
|
||||
console.error('Error in health subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if system is healthy
|
||||
isSystemHealthy() {
|
||||
if (!this.lastHealthStatus) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.status === 'healthy';
|
||||
}
|
||||
|
||||
// Get component status
|
||||
getComponentStatus(componentName) {
|
||||
if (!this.lastHealthStatus?.components) {
|
||||
return null;
|
||||
}
|
||||
return this.lastHealthStatus.components[componentName];
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopHealthMonitoring();
|
||||
this.healthSubscribers = [];
|
||||
this.lastHealthStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const healthService = new HealthService();
|
||||
303
ui/services/pose.service.js
Normal file
303
ui/services/pose.service.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// Pose Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
import { wsService } from './websocket.service.js';
|
||||
|
||||
export class PoseService {
|
||||
constructor() {
|
||||
this.streamConnection = null;
|
||||
this.eventConnection = null;
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
|
||||
// Get current pose estimation
|
||||
async getCurrentPose(options = {}) {
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
confidence_threshold: options.confidenceThreshold,
|
||||
max_persons: options.maxPersons,
|
||||
include_keypoints: options.includeKeypoints,
|
||||
include_segmentation: options.includeSegmentation
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CURRENT, params);
|
||||
}
|
||||
|
||||
// Analyze pose (requires auth)
|
||||
async analyzePose(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.ANALYZE, request);
|
||||
}
|
||||
|
||||
// Get zone occupancy
|
||||
async getZoneOccupancy(zoneId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.POSE.ZONE_OCCUPANCY.replace('{zone_id}', zoneId);
|
||||
return apiService.get(endpoint);
|
||||
}
|
||||
|
||||
// Get zones summary
|
||||
async getZonesSummary() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ZONES_SUMMARY);
|
||||
}
|
||||
|
||||
// Get historical data (requires auth)
|
||||
async getHistoricalData(request) {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.HISTORICAL, request);
|
||||
}
|
||||
|
||||
// Get recent activities
|
||||
async getActivities(options = {}) {
|
||||
const params = {
|
||||
zone_id: options.zoneId,
|
||||
limit: options.limit || 50
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ACTIVITIES, params);
|
||||
}
|
||||
|
||||
// Calibrate system (requires auth)
|
||||
async calibrate() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.POSE.CALIBRATE);
|
||||
}
|
||||
|
||||
// Get calibration status (requires auth)
|
||||
async getCalibrationStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CALIBRATION_STATUS);
|
||||
}
|
||||
|
||||
// Get pose statistics
|
||||
async getStats(hours = 24) {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.POSE.STATS, { hours });
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
startPoseStream(options = {}) {
|
||||
if (this.streamConnection) {
|
||||
console.warn('Pose stream already active');
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
min_confidence: options.minConfidence || 0.5,
|
||||
max_fps: options.maxFps || 30,
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.streamConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_POSE,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Pose stream connected');
|
||||
this.notifyPoseSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handlePoseMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Pose stream error:', error);
|
||||
this.notifyPoseSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Pose stream disconnected');
|
||||
this.streamConnection = null;
|
||||
this.notifyPoseSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.streamConnection;
|
||||
}
|
||||
|
||||
// Stop pose stream
|
||||
stopPoseStream() {
|
||||
if (this.streamConnection) {
|
||||
wsService.disconnect(this.streamConnection);
|
||||
this.streamConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to pose updates
|
||||
subscribeToPoseUpdates(callback) {
|
||||
this.poseSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.poseSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.poseSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle pose stream messages
|
||||
handlePoseMessage(data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
switch (type) {
|
||||
case 'pose_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'pose_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'historical_data':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'historical_update',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'zone_statistics':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'zone_stats',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
case 'system_event':
|
||||
this.notifyPoseSubscribers({
|
||||
type: 'system_event',
|
||||
data: payload
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown pose message type:', type);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify pose subscribers
|
||||
notifyPoseSubscribers(update) {
|
||||
this.poseSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in pose subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start event stream
|
||||
startEventStream(options = {}) {
|
||||
if (this.eventConnection) {
|
||||
console.warn('Event stream already active');
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
const params = {
|
||||
event_types: options.eventTypes?.join(','),
|
||||
zone_ids: options.zoneIds?.join(','),
|
||||
token: options.token || apiService.authToken
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
this.eventConnection = wsService.connect(
|
||||
API_CONFIG.ENDPOINTS.STREAM.WS_EVENTS,
|
||||
params,
|
||||
{
|
||||
onOpen: () => {
|
||||
console.log('Event stream connected');
|
||||
this.notifyEventSubscribers({ type: 'connected' });
|
||||
},
|
||||
onMessage: (data) => {
|
||||
this.handleEventMessage(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Event stream error:', error);
|
||||
this.notifyEventSubscribers({ type: 'error', error });
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Event stream disconnected');
|
||||
this.eventConnection = null;
|
||||
this.notifyEventSubscribers({ type: 'disconnected' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return this.eventConnection;
|
||||
}
|
||||
|
||||
// Stop event stream
|
||||
stopEventStream() {
|
||||
if (this.eventConnection) {
|
||||
wsService.disconnect(this.eventConnection);
|
||||
this.eventConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
subscribeToEvents(callback) {
|
||||
this.eventSubscribers.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.eventSubscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.eventSubscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle event stream messages
|
||||
handleEventMessage(data) {
|
||||
this.notifyEventSubscribers({
|
||||
type: 'event',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// Notify event subscribers
|
||||
notifyEventSubscribers(update) {
|
||||
this.eventSubscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update);
|
||||
} catch (error) {
|
||||
console.error('Error in event subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update stream configuration
|
||||
updateStreamConfig(connectionId, config) {
|
||||
wsService.sendCommand(connectionId, 'update_config', config);
|
||||
}
|
||||
|
||||
// Get stream status
|
||||
requestStreamStatus(connectionId) {
|
||||
wsService.sendCommand(connectionId, 'get_status');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopPoseStream();
|
||||
this.stopEventStream();
|
||||
this.poseSubscribers = [];
|
||||
this.eventSubscribers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const poseService = new PoseService();
|
||||
59
ui/services/stream.service.js
Normal file
59
ui/services/stream.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Stream Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG } from '../config/api.config.js';
|
||||
import { apiService } from './api.service.js';
|
||||
|
||||
export class StreamService {
|
||||
// Get streaming status
|
||||
async getStatus() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.STATUS);
|
||||
}
|
||||
|
||||
// Start streaming (requires auth)
|
||||
async start() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.START);
|
||||
}
|
||||
|
||||
// Stop streaming (requires auth)
|
||||
async stop() {
|
||||
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.STOP);
|
||||
}
|
||||
|
||||
// Get connected clients (requires auth)
|
||||
async getClients() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.CLIENTS);
|
||||
}
|
||||
|
||||
// Disconnect a client (requires auth)
|
||||
async disconnectClient(clientId) {
|
||||
const endpoint = API_CONFIG.ENDPOINTS.STREAM.DISCONNECT_CLIENT.replace('{client_id}', clientId);
|
||||
return apiService.delete(endpoint);
|
||||
}
|
||||
|
||||
// Broadcast message (requires auth)
|
||||
async broadcast(message, options = {}) {
|
||||
const params = {
|
||||
stream_type: options.streamType,
|
||||
zone_ids: options.zoneIds?.join(',')
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(params).forEach(key =>
|
||||
params[key] === undefined && delete params[key]
|
||||
);
|
||||
|
||||
return apiService.post(
|
||||
API_CONFIG.ENDPOINTS.STREAM.BROADCAST,
|
||||
message,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
// Get streaming metrics
|
||||
async getMetrics() {
|
||||
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.METRICS);
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const streamService = new StreamService();
|
||||
305
ui/services/websocket.service.js
Normal file
305
ui/services/websocket.service.js
Normal file
@@ -0,0 +1,305 @@
|
||||
// WebSocket Service for WiFi-DensePose UI
|
||||
|
||||
import { API_CONFIG, buildWsUrl } from '../config/api.config.js';
|
||||
|
||||
export class WebSocketService {
|
||||
constructor() {
|
||||
this.connections = new Map();
|
||||
this.messageHandlers = new Map();
|
||||
this.reconnectAttempts = new Map();
|
||||
}
|
||||
|
||||
// Connect to WebSocket endpoint
|
||||
connect(endpoint, params = {}, handlers = {}) {
|
||||
const url = buildWsUrl(endpoint, params);
|
||||
|
||||
// Check if already connected
|
||||
if (this.connections.has(url)) {
|
||||
console.warn(`Already connected to ${url}`);
|
||||
return this.connections.get(url);
|
||||
}
|
||||
|
||||
// Create WebSocket connection
|
||||
const ws = new WebSocket(url);
|
||||
const connectionId = this.generateId();
|
||||
|
||||
// Store connection
|
||||
this.connections.set(url, {
|
||||
id: connectionId,
|
||||
ws,
|
||||
url,
|
||||
handlers,
|
||||
status: 'connecting',
|
||||
lastPing: null,
|
||||
reconnectTimer: null
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
this.setupEventHandlers(url, ws, handlers);
|
||||
|
||||
// Start ping interval
|
||||
this.startPingInterval(url);
|
||||
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
// Set up WebSocket event handlers
|
||||
setupEventHandlers(url, ws, handlers) {
|
||||
const connection = this.connections.get(url);
|
||||
|
||||
ws.onopen = (event) => {
|
||||
console.log(`WebSocket connected: ${url}`);
|
||||
connection.status = 'connected';
|
||||
this.reconnectAttempts.set(url, 0);
|
||||
|
||||
if (handlers.onOpen) {
|
||||
handlers.onOpen(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle different message types
|
||||
this.handleMessage(url, data);
|
||||
|
||||
if (handlers.onMessage) {
|
||||
handlers.onMessage(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error(`WebSocket error: ${url}`, event);
|
||||
connection.status = 'error';
|
||||
|
||||
if (handlers.onError) {
|
||||
handlers.onError(event);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`WebSocket closed: ${url}`);
|
||||
connection.status = 'closed';
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(url);
|
||||
|
||||
if (handlers.onClose) {
|
||||
handlers.onClose(event);
|
||||
}
|
||||
|
||||
// Attempt reconnection if not intentionally closed
|
||||
if (!event.wasClean && this.shouldReconnect(url)) {
|
||||
this.scheduleReconnect(url);
|
||||
} else {
|
||||
this.connections.delete(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
handleMessage(url, data) {
|
||||
const { type, payload } = data;
|
||||
|
||||
// Handle system messages
|
||||
switch (type) {
|
||||
case 'pong':
|
||||
this.handlePong(url);
|
||||
break;
|
||||
|
||||
case 'connection_established':
|
||||
console.log('Connection established:', payload);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('WebSocket error message:', payload);
|
||||
break;
|
||||
}
|
||||
|
||||
// Call registered message handlers
|
||||
const handlers = this.messageHandlers.get(url) || [];
|
||||
handlers.forEach(handler => handler(data));
|
||||
}
|
||||
|
||||
// Send message through WebSocket
|
||||
send(connectionId, message) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (connection.status !== 'connected') {
|
||||
throw new Error(`Connection ${connectionId} is not connected`);
|
||||
}
|
||||
|
||||
const data = typeof message === 'string'
|
||||
? message
|
||||
: JSON.stringify(message);
|
||||
|
||||
connection.ws.send(data);
|
||||
}
|
||||
|
||||
// Send command message
|
||||
sendCommand(connectionId, command, payload = {}) {
|
||||
this.send(connectionId, {
|
||||
type: command,
|
||||
payload,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Register message handler
|
||||
onMessage(connectionId, handler) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
if (!this.messageHandlers.has(connection.url)) {
|
||||
this.messageHandlers.set(connection.url, []);
|
||||
}
|
||||
|
||||
this.messageHandlers.get(connection.url).push(handler);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const handlers = this.messageHandlers.get(connection.url);
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Disconnect WebSocket
|
||||
disconnect(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear reconnection timer
|
||||
if (connection.reconnectTimer) {
|
||||
clearTimeout(connection.reconnectTimer);
|
||||
}
|
||||
|
||||
// Clear ping interval
|
||||
this.clearPingInterval(connection.url);
|
||||
|
||||
// Close WebSocket
|
||||
if (connection.ws.readyState === WebSocket.OPEN) {
|
||||
connection.ws.close(1000, 'Client disconnect');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
this.connections.delete(connection.url);
|
||||
this.messageHandlers.delete(connection.url);
|
||||
this.reconnectAttempts.delete(connection.url);
|
||||
}
|
||||
|
||||
// Disconnect all WebSockets
|
||||
disconnectAll() {
|
||||
const connectionIds = Array.from(this.connections.values()).map(c => c.id);
|
||||
connectionIds.forEach(id => this.disconnect(id));
|
||||
}
|
||||
|
||||
// Ping/Pong handling
|
||||
startPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
connection.pingInterval = setInterval(() => {
|
||||
if (connection.status === 'connected') {
|
||||
this.sendPing(url);
|
||||
}
|
||||
}, API_CONFIG.WS_CONFIG.PING_INTERVAL);
|
||||
}
|
||||
|
||||
clearPingInterval(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.pingInterval) {
|
||||
clearInterval(connection.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
sendPing(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection && connection.status === 'connected') {
|
||||
connection.lastPing = Date.now();
|
||||
connection.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}
|
||||
|
||||
handlePong(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (connection) {
|
||||
const latency = Date.now() - connection.lastPing;
|
||||
console.log(`Pong received. Latency: ${latency}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnection logic
|
||||
shouldReconnect(url) {
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
return attempts < API_CONFIG.WS_CONFIG.MAX_RECONNECT_ATTEMPTS;
|
||||
}
|
||||
|
||||
scheduleReconnect(url) {
|
||||
const connection = this.connections.get(url);
|
||||
if (!connection) return;
|
||||
|
||||
const attempts = this.reconnectAttempts.get(url) || 0;
|
||||
const delay = API_CONFIG.WS_CONFIG.RECONNECT_DELAY * Math.pow(2, attempts);
|
||||
|
||||
console.log(`Scheduling reconnect in ${delay}ms (attempt ${attempts + 1})`);
|
||||
|
||||
connection.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts.set(url, attempts + 1);
|
||||
|
||||
// Get original parameters
|
||||
const params = new URL(url).searchParams;
|
||||
const paramsObj = Object.fromEntries(params);
|
||||
const endpoint = url.replace(/^wss?:\/\/[^\/]+/, '').split('?')[0];
|
||||
|
||||
// Attempt reconnection
|
||||
this.connect(endpoint, paramsObj, connection.handlers);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
findConnectionById(connectionId) {
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.id === connectionId) {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
getConnectionStatus(connectionId) {
|
||||
const connection = this.findConnectionById(connectionId);
|
||||
return connection ? connection.status : 'disconnected';
|
||||
}
|
||||
|
||||
getActiveConnections() {
|
||||
return Array.from(this.connections.values()).map(conn => ({
|
||||
id: conn.id,
|
||||
url: conn.url,
|
||||
status: conn.status
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const wsService = new WebSocketService();
|
||||
Reference in New Issue
Block a user