websocket.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. /**
  2. * WebSocket client for real-time updates
  3. */
  4. class WebSocketClient {
  5. constructor(basePath = '') {
  6. this.basePath = basePath;
  7. this.ws = null;
  8. this.reconnectAttempts = 0;
  9. this.maxReconnectAttempts = 10;
  10. this.reconnectDelay = 1000;
  11. this.listeners = new Map();
  12. this.isConnected = false;
  13. this.shouldReconnect = true;
  14. }
  15. connect() {
  16. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  17. return;
  18. }
  19. const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  20. // Ensure basePath ends with '/' for proper URL construction
  21. let basePath = this.basePath || '';
  22. if (basePath && !basePath.endsWith('/')) {
  23. basePath += '/';
  24. }
  25. const wsUrl = `${protocol}//${window.location.host}${basePath}ws`;
  26. console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath);
  27. try {
  28. this.ws = new WebSocket(wsUrl);
  29. this.ws.onopen = () => {
  30. console.log('WebSocket connected');
  31. this.isConnected = true;
  32. this.reconnectAttempts = 0;
  33. this.emit('connected');
  34. };
  35. this.ws.onmessage = (event) => {
  36. try {
  37. // Validate message size (prevent memory issues)
  38. const maxMessageSize = 10 * 1024 * 1024; // 10MB
  39. if (event.data && event.data.length > maxMessageSize) {
  40. console.error('WebSocket message too large:', event.data.length, 'bytes');
  41. this.ws.close();
  42. return;
  43. }
  44. const message = JSON.parse(event.data);
  45. if (!message || typeof message !== 'object') {
  46. console.error('Invalid WebSocket message format');
  47. return;
  48. }
  49. this.handleMessage(message);
  50. } catch (e) {
  51. console.error('Failed to parse WebSocket message:', e);
  52. }
  53. };
  54. this.ws.onerror = (error) => {
  55. console.error('WebSocket error:', error);
  56. this.emit('error', error);
  57. };
  58. this.ws.onclose = () => {
  59. console.log('WebSocket disconnected');
  60. this.isConnected = false;
  61. this.emit('disconnected');
  62. if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
  63. this.reconnectAttempts++;
  64. const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
  65. console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
  66. setTimeout(() => this.connect(), delay);
  67. }
  68. };
  69. } catch (e) {
  70. console.error('Failed to create WebSocket connection:', e);
  71. this.emit('error', e);
  72. }
  73. }
  74. handleMessage(message) {
  75. const { type, payload, time } = message;
  76. // Emit to specific type listeners
  77. this.emit(type, payload, time);
  78. // Emit to all listeners
  79. this.emit('message', { type, payload, time });
  80. }
  81. on(event, callback) {
  82. if (!this.listeners.has(event)) {
  83. this.listeners.set(event, []);
  84. }
  85. this.listeners.get(event).push(callback);
  86. }
  87. off(event, callback) {
  88. if (!this.listeners.has(event)) {
  89. return;
  90. }
  91. const callbacks = this.listeners.get(event);
  92. const index = callbacks.indexOf(callback);
  93. if (index > -1) {
  94. callbacks.splice(index, 1);
  95. }
  96. }
  97. emit(event, ...args) {
  98. if (this.listeners.has(event)) {
  99. this.listeners.get(event).forEach(callback => {
  100. try {
  101. callback(...args);
  102. } catch (e) {
  103. console.error('Error in WebSocket event handler:', e);
  104. }
  105. });
  106. }
  107. }
  108. disconnect() {
  109. this.shouldReconnect = false;
  110. if (this.ws) {
  111. this.ws.close();
  112. this.ws = null;
  113. }
  114. }
  115. send(data) {
  116. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  117. this.ws.send(JSON.stringify(data));
  118. } else {
  119. console.warn('WebSocket is not connected');
  120. }
  121. }
  122. }
  123. // Create global WebSocket client instance
  124. // Safely get basePath from global scope (defined in page.html)
  125. window.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : '');