Files
idle-xboct/packages/overlay/src/net.ts
T

66 lines
2.0 KiB
TypeScript

import { PROTOCOL_VERSION, type ClientMessage, type DeltaMsg, type ServerMessage, type SnapshotMsg, type WelcomeMsg } from '@idle/shared';
export type NetStatus = 'connecting' | 'online' | 'reconnecting';
export function wsUrl(): string {
const fromEnv = import.meta.env.VITE_WS_URL;
if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv;
// сборка раздаётся самим сервером — тот же origin
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
return `${scheme}://${location.host}/ws`;
}
export interface ConnectHandlers {
onStatus: (status: NetStatus) => void;
onWelcome: (msg: WelcomeMsg) => void;
onSnapshot: (msg: SnapshotMsg) => void;
onDelta: (msg: DeltaMsg) => void;
}
/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */
export function connectWorld(handlers: ConnectHandlers): () => void {
let ws: WebSocket | null = null;
let disposed = false;
let attempt = 0;
const open = (): void => {
if (disposed) return;
handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting');
ws = new WebSocket(wsUrl());
ws.onopen = () => {
attempt = 0;
const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION };
ws?.send(JSON.stringify(hello));
};
ws.onmessage = (ev) => {
let msg: ServerMessage;
try {
msg = JSON.parse(String(ev.data)) as ServerMessage;
} catch {
return;
}
if (msg.t === 'welcome') {
handlers.onStatus('online');
handlers.onWelcome(msg);
} else if (msg.t === 'snapshot') {
handlers.onSnapshot(msg);
} else if (msg.t === 'delta') {
handlers.onDelta(msg);
}
};
ws.onclose = () => {
if (disposed) return;
attempt += 1;
setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000));
};
ws.onerror = () => ws?.close();
};
open();
return () => {
disposed = true;
ws?.close();
};
}