95 lines
4.1 KiB
JavaScript
95 lines
4.1 KiB
JavaScript
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
import { logError, logInfo, logWarn } from "@rustpad/logger";
|
|
|
|
class RoomSocket {
|
|
constructor(options) {
|
|
Object.assign(this, options);
|
|
this.socket = null;
|
|
this.timer = null;
|
|
this.pingTimer = null;
|
|
this.closed = false;
|
|
this.pendingPings = new Map();
|
|
}
|
|
get url() { throw new Error("Socket URL not implemented"); }
|
|
get kind() { return "room"; }
|
|
connect() {
|
|
clearTimeout(this.timer);
|
|
clearInterval(this.pingTimer);
|
|
this.closed = false;
|
|
this.onStatus?.("connecting");
|
|
this.socket = new WebSocket(this.url);
|
|
this.socket.addEventListener("open", () => {
|
|
logInfo("websocket.open", { kind: this.kind });
|
|
this.send({ type: "authenticate", password: this.password || null, access_token: this.accessToken || null, nickname: this.nickname || null, session_token: this.sessionToken || null, guest_id: this.guestId || null, color: this.color || null });
|
|
});
|
|
this.socket.addEventListener("message", event => {
|
|
let message;
|
|
try { message = JSON.parse(event.data); } catch { return; }
|
|
if (message.type === "error") { this.onError?.(message.message); this.closed = true; this.socket.close(); return; }
|
|
if (message.type === "authenticated") {
|
|
this.onStatus?.("online");
|
|
this.onAuthenticated?.(message);
|
|
this.startPing();
|
|
return;
|
|
}
|
|
if (message.type === "document") this.onDocument?.(message);
|
|
if (message.type === "presence") this.onPresence?.(message.users || []);
|
|
if (message.type === "chat") this.onChat?.(message);
|
|
if (message.type === "pong") {
|
|
const started = this.pendingPings.get(message.nonce);
|
|
if (started !== undefined) {
|
|
this.pendingPings.delete(message.nonce);
|
|
this.onLatency?.(Math.max(0, Math.round(performance.now() - started)));
|
|
}
|
|
}
|
|
});
|
|
this.socket.addEventListener("close", event => {
|
|
clearInterval(this.pingTimer);
|
|
this.pendingPings.clear();
|
|
this.onPresence?.([]);
|
|
this.onLatency?.(null);
|
|
logWarn("websocket.close", { kind: this.kind, code: event.code, reason: event.reason || "", intentional: this.closed });
|
|
if (!this.closed) { this.onStatus?.("offline"); this.timer = setTimeout(() => this.connect(), 1500); }
|
|
});
|
|
this.socket.addEventListener("error", event => {
|
|
logError("websocket.error", event, { kind: this.kind });
|
|
this.onError?.("Failed to connect to the WebSocket server");
|
|
this.socket.close();
|
|
});
|
|
}
|
|
startPing() {
|
|
clearInterval(this.pingTimer);
|
|
const ping = () => {
|
|
if (this.socket?.readyState !== WebSocket.OPEN) return;
|
|
const nonce = Date.now();
|
|
this.pendingPings.set(nonce, performance.now());
|
|
for (const key of this.pendingPings.keys()) if (key < nonce - 30000) this.pendingPings.delete(key);
|
|
this.send({ type: "ping", nonce });
|
|
};
|
|
ping();
|
|
this.pingTimer = setInterval(ping, 10000);
|
|
}
|
|
send(message) { if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message)); }
|
|
update(content, ownerMap = "[]") { this.send({ type: "update", content, owner_map: ownerMap }); }
|
|
chat(text) { this.send({ type: "chat", text }); }
|
|
setColor(color) { this.color = color || null; this.send({ type: "set_color", color: this.color }); }
|
|
stop() { this.closed = true; clearTimeout(this.timer); clearInterval(this.pingTimer); this.socket?.close(); }
|
|
}
|
|
|
|
export class NoteSocket extends RoomSocket {
|
|
get kind() { return "note"; }
|
|
get url() { const p = location.protocol === "https:" ? "wss:" : "ws:"; return `${p}//${location.host}/ws/${encodeURIComponent(this.workspaceSlug)}/${encodeURIComponent(this.noteSlug)}`; }
|
|
}
|
|
export class PadSocket extends RoomSocket {
|
|
get kind() { return "pad"; }
|
|
get url() { const p = location.protocol === "https:" ? "wss:" : "ws:"; return `${p}//${location.host}/ws/p/${encodeURIComponent(this.slug)}`; }
|
|
}
|