Adds msgPack and update message protocol

This commit is contained in:
Rune Harlyk
2025-07-10 16:18:09 +02:00
committed by Rune Harlyk
parent 01d46f283b
commit a43c250ed1
9 changed files with 485 additions and 321 deletions
+139 -101
View File
@@ -1,122 +1,160 @@
import { writable } from 'svelte/store';
import msgpack from 'msgpack-lite';
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
type SocketEvent = (typeof socketEvents)[number];
type SocketMessage = [number, string?, unknown?];
let useBinary = false;
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer;
try {
if (useBinary) {
return msgpack.decode(new Uint8Array(data as ArrayBuffer));
}
return JSON.parse(data as string);
} catch (error) {
console.error(`Could not decode data: ${data} - ${error}`);
}
return null;
};
const encodeMessage = (data: unknown) => {
try {
return useBinary ? msgpack.encode(data) : JSON.stringify(data);
} catch (error) {
console.error(`Could not encode data: ${data} - ${error}`);
}
};
function createWebSocket() {
let listeners = new Map<string, Set<(data?: unknown) => void>>();
const { subscribe, set } = writable(false);
const reconnectTimeoutTime = 5000;
let unresponsiveTimeoutId: number;
let reconnectTimeoutId: number;
let ws: WebSocket;
let socketUrl: string | URL;
const listeners = new Map<string, Set<(data?: unknown) => void>>();
const { subscribe, set } = writable(false);
const reconnectTimeoutTime = 5000;
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
let reconnectTimeoutId: ReturnType<typeof setTimeout>;
let ws: WebSocket;
let socketUrl: string | URL;
function init(url: string | URL) {
socketUrl = url;
connect();
}
function init(url: string | URL) {
socketUrl = url;
connect();
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach((listener) => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach(listener => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function connect() {
ws = new WebSocket(socketUrl);
ws.onopen = (ev) => {
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach((listener) => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
};
ws.onmessage = (message) => {
resetUnresponsiveCheck();
let data = message.data;
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach((listener) => listener(data));
return;
}
data = data.substring(1);
function connect() {
ws = new WebSocket(socketUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = ev => {
ping();
useBinary = true;
ping();
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach(listener => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
};
ws.onmessage = frame => {
resetUnresponsiveCheck();
const message = decodeMessage(frame.data);
if (!message) return;
const [_, event, payload = undefined] = message;
if (event) listeners.get(event)?.forEach(listener => listener(payload));
};
ws.onerror = ev => disconnect('error', ev);
ws.onclose = ev => disconnect('close', ev);
}
if (!data) return;
function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
if (!eventListeners.size) {
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
try {
payload = JSON.parse(payload);
} catch (error) {}
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
};
ws.onerror = (ev) => disconnect('error', ev);
ws.onclose = (ev) => disconnect('close', ev);
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function unsubscribe(event: string, listener?: (data: any) => void) {
let eventListeners = listeners.get(event);
if (!eventListeners) return;
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([2, event, data]);
}
if (!eventListeners.size) {
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([1, event]);
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([0, event]);
}
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(`2/${event}[${JSON.stringify(data)}]`);
}
function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const serialized = encodeMessage(data);
if (!serialized) {
console.error('Could not serialize data:', data);
return;
}
ws.send(serialized);
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('1/' + event);
}
function ping() {
const serialized = encodeMessage([4]);
if (!serialized) {
console.error('Could not serialize message');
return;
}
ws.send(serialized);
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('0/' + event);
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: unknown) => void);
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void);
return () => {
unsubscribe(event, listener);
};
},
off: (event: string, listener?: (data: any) => void) => {
unsubscribe(event, listener);
}
};
return () => {
unsubscribe(event, listener as (data: unknown) => void);
};
},
off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void);
},
};
}
export const socket = createWebSocket();
export const socket = createWebSocket();