chat / js /ws.js
incognitolm's picture
Initial
5383ef0 verified
Raw
History Blame
2.8 kB
// ws.js - WebSocket connection manager
const WS_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`;
const RECONNECT_DELAY_MS = 2000;
const MAX_RECONNECT_DELAY = 30000;
let ws = null;
let reconnectDelay = RECONNECT_DELAY_MS;
let reconnectTimer = null;
let pendingCallbacks = new Map(); // id -> { resolve, reject, timeout }
let msgId = 0;
const listeners = new Map(); // type -> Set<fn>
export function send(data) {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
return true;
}
return false;
}
export function request(data, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const id = `req_${++msgId}`;
const full = { ...data, _reqId: id };
const timer = setTimeout(() => {
pendingCallbacks.delete(id);
reject(new Error('Request timeout'));
}, timeoutMs);
pendingCallbacks.set(id, { resolve, reject, timer });
if (!send(full)) {
clearTimeout(timer);
pendingCallbacks.delete(id);
reject(new Error('WebSocket not connected'));
}
});
}
export function on(type, fn) {
if (!listeners.has(type)) listeners.set(type, new Set());
listeners.get(type).add(fn);
return () => listeners.get(type)?.delete(fn);
}
export function off(type, fn) {
listeners.get(type)?.delete(fn);
}
function emit(type, data) {
listeners.get(type)?.forEach(fn => fn(data));
listeners.get('*')?.forEach(fn => fn({ type, ...data }));
}
function connect() {
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return;
ws = new WebSocket(WS_URL);
ws.onopen = () => {
reconnectDelay = RECONNECT_DELAY_MS;
emit('ws:connected', {});
};
ws.onmessage = (event) => {
let msg;
try { msg = JSON.parse(event.data); } catch { return; }
if (!msg?.type) return;
// Resolve pending request callbacks
if (msg._reqId && pendingCallbacks.has(msg._reqId)) {
const cb = pendingCallbacks.get(msg._reqId);
pendingCallbacks.delete(msg._reqId);
clearTimeout(cb.timer);
if (msg.error) cb.reject(new Error(msg.error));
else cb.resolve(msg);
return;
}
emit(msg.type, msg);
};
ws.onclose = () => {
emit('ws:disconnected', {});
scheduleReconnect();
};
ws.onerror = () => {
ws?.close();
};
}
function scheduleReconnect() {
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
connect();
}, reconnectDelay);
}
export function getReadyState() {
return ws?.readyState ?? WebSocket.CLOSED;
}
export function isConnected() {
return ws?.readyState === WebSocket.OPEN;
}
// Boot
connect();
window.ws = { send, request, on, off, isConnected, getReadyState };