beeta/frontend/src/lib/websocket.js
doomtube 118629549e Fix: Use dynamic URLs for all frontend connections
- CSP: Allow WebSocket/HTTP connections to any domain (for production)
- Nakama: Detect host/SSL from browser location instead of hardcoded localhost
- WebSocket: Dynamic protocol/host detection for stream and watch sync
- HLS/LLHLS/WebRTC: Dynamic URLs in live page and stream components
- RTMP/SRT: Show actual domain in my-realms settings page
- Forums: Use numeric forum ID for banner/title-color API calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 04:45:39 -05:00

104 lines
No EOL
3 KiB
JavaScript

import { browser } from '$app/environment';
let ws = null;
let reconnectTimeout = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;
const BASE_RECONNECT_DELAY = 1000; // 1 second
const MAX_RECONNECT_DELAY = 30000; // 30 seconds
// Dynamically detect WebSocket URL from browser location
// This ensures production uses wss:// and the correct host
function getWebSocketURL() {
if (!browser) {
return import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}/ws`;
}
const WS_URL = getWebSocketURL();
/**
* Calculate exponential backoff delay with jitter
* @returns {number} Delay in milliseconds
*/
function getReconnectDelay() {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts);
const cappedDelay = Math.min(exponentialDelay, MAX_RECONNECT_DELAY);
// Add random jitter (±20%) to prevent thundering herd
const jitter = cappedDelay * 0.2 * (Math.random() - 0.5);
return Math.floor(cappedDelay + jitter);
}
export function connectWebSocket(onMessage) {
if (ws?.readyState === WebSocket.OPEN) return;
// WebSocket doesn't support withCredentials, but cookies are sent automatically
// on same-origin requests
ws = new WebSocket(`${WS_URL}/stream`);
ws.onopen = () => {
console.log('WebSocket connected');
reconnectAttempts = 0; // Reset on successful connection
ws?.send(JSON.stringify({ type: 'subscribe' }));
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onMessage(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error('Max reconnect attempts reached, giving up');
return;
}
const delay = getReconnectDelay();
reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
reconnectTimeout = setTimeout(() => {
connectWebSocket(onMessage);
}, delay);
};
}
export function disconnectWebSocket() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (ws) {
ws.close();
ws = null;
}
reconnectAttempts = 0; // Reset attempts on explicit disconnect
}
/**
* Reset reconnect attempts counter (call this to allow reconnection after max attempts)
*/
export function resetReconnectAttempts() {
reconnectAttempts = 0;
}
export function sendMessage(message) {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}