Files

107 lines
3.3 KiB
JavaScript
Raw Permalink Normal View History

2026-10-06 15:58:02 +02:00
// @ts-check
/**
* The connection to the relay. It knows nothing about pages or encryption:
* it delivers bytes to peers and reports who is present. It reconnects by
* itself when the connection drops.
*/
const RETRY_DELAYS_MS = [500, 1000, 2000, 5000];
/** Close codes after which retrying is pointless (see the relay). */
const FINAL_CLOSE_CODES = new Map([
[4001, 'The session is full.'],
[4002, 'The relay is full.'],
]);
/**
* @typedef {object} RelayHandlers
* @property {(peer: number, peers: number[]) => void} onWelcome
* Connected (or reconnected): our peer number and the peers present.
* @property {(peer: number) => void} onJoin
* @property {(peer: number) => void} onLeave
* @property {(peer: number, body: Uint8Array<ArrayBuffer>) => void} onMessage
* @property {(status: 'connecting' | 'connected' | 'failed', detail?: string) => void} onStatus
*/
export class RelayConnection {
/**
* @param {string} relay Address of the relay, ws:// or wss://.
* @param {string} roomId
* @param {RelayHandlers} handlers
*/
constructor(relay, roomId, handlers) {
this.url = `${relay}/room/${roomId}`;
this.handlers = handlers;
/** @type {WebSocket | null} */
this.socket = null;
this.attempt = 0;
this.closed = false;
}
connect() {
if (this.closed) return;
this.handlers.onStatus('connecting');
const socket = new WebSocket(this.url);
socket.binaryType = 'arraybuffer';
this.socket = socket;
socket.onmessage = (event) => {
if (socket !== this.socket) return;
if (typeof event.data === 'string') {
const control = JSON.parse(event.data);
if (control.type === 'welcome') {
this.attempt = 0;
this.handlers.onStatus('connected');
this.handlers.onWelcome(control.peer, control.peers);
} else if (control.type === 'join') {
this.handlers.onJoin(control.peer);
} else if (control.type === 'leave') {
this.handlers.onLeave(control.peer);
}
} else {
const bytes = new Uint8Array(event.data);
if (bytes.length < 4) return;
const peer = new DataView(event.data).getUint32(0);
this.handlers.onMessage(peer, bytes.subarray(4));
}
};
socket.onclose = (event) => {
if (socket !== this.socket) return;
this.socket = null;
if (this.closed) return;
const final = FINAL_CLOSE_CODES.get(event.code);
if (final) {
this.closed = true;
this.handlers.onStatus('failed', final);
return;
}
this.handlers.onStatus('connecting');
const delay = RETRY_DELAYS_MS[Math.min(this.attempt, RETRY_DELAYS_MS.length - 1)];
this.attempt += 1;
setTimeout(() => this.connect(), delay);
};
}
/** @returns {boolean} */
get connected() {
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
}
/**
* @param {number} destination A peer number, or 0 for everyone else.
* @param {Uint8Array} body
*/
send(destination, body) {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
const message = new Uint8Array(4 + body.length);
new DataView(message.buffer).setUint32(0, destination);
message.set(body, 4);
this.socket.send(message);
}
close() {
this.closed = true;
if (this.socket) this.socket.close();
}
}