Files
web-cad/frontend/src/crdt/WebSocketProvider.ts
T
Leopoldadmin 6d15a692af fix: WebSocket wss:// same-origin URL + try/catch to prevent crash on HTTPS
- Change DEFAULT_WS_URL from ws://hostname:3001 to wss://window.location.host
- Wrap WebSocket constructor in try/catch to prevent React crash on connection error
- Fixes white page when opening a project (CADEditor crashed on insecure ws:// from HTTPS page)
2026-06-26 17:58:06 +02:00

145 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WebSocketProvider Custom WebSocket provider matching the backend raw-update protocol.
* Backend sends Y.encodeStateAsUpdate on connect and applies raw updates on message.
*/
import * as Y from 'yjs';
import type { YjsDocument } from './YjsDocument';
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
export interface WebSocketProviderOptions {
url: string;
docName: string;
yjsDoc: YjsDocument;
onStatusChange?: (status: ConnectionStatus) => void;
onSync?: () => void;
}
export class WebSocketProvider {
private ws: WebSocket | null = null;
private url: string;
private docName: string;
private yjsDoc: YjsDocument;
private status: ConnectionStatus = 'disconnected';
private onStatusChange?: (status: ConnectionStatus) => void;
private onSync?: () => void;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
private shouldReconnect = true;
constructor(opts: WebSocketProviderOptions) {
this.url = opts.url;
this.docName = opts.docName;
this.yjsDoc = opts.yjsDoc;
this.onStatusChange = opts.onStatusChange;
this.onSync = opts.onSync;
}
connect(): void {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
this.shouldReconnect = true;
this.setStatus('connecting');
const fullUrl = `${this.url}/ws/collab/${encodeURIComponent(this.docName)}`;
try {
this.ws = new WebSocket(fullUrl);
} catch (err) {
console.warn('[WebSocketProvider] Failed to construct WebSocket:', err);
this.setStatus('error');
this.scheduleReconnect();
return;
}
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.setStatus('connected');
this.reconnectDelay = 1000;
// Send local state to server for initial sync
const stateUpdate = Y.encodeStateAsUpdate(this.yjsDoc.doc);
if (stateUpdate.length > 0) {
this.ws?.send(stateUpdate);
}
this.onSync?.();
};
this.ws.onmessage = (event: MessageEvent) => {
try {
const data = new Uint8Array(event.data as ArrayBuffer);
this.yjsDoc.applyUpdate(data);
} catch (err) {
console.warn('[WebSocketProvider] Failed to apply update:', err);
}
};
this.ws.onerror = () => {
this.setStatus('error');
};
this.ws.onclose = () => {
this.ws = null;
this.setStatus('disconnected');
if (this.shouldReconnect) {
this.scheduleReconnect();
}
};
}
/** Send a local Y.Doc update to the server */
sendUpdate(update: Uint8Array): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(update);
}
}
/** Listen for local doc changes and forward to server */
bindLocalUpdates(): () => void {
const handler = (update: Uint8Array, origin: unknown) => {
// Only send updates that originated locally (not from remote apply)
if (origin !== 'remote') {
this.sendUpdate(update);
}
};
this.yjsDoc.doc.on('update', handler);
return () => {
this.yjsDoc.doc.off('update', handler);
};
}
getStatus(): ConnectionStatus {
return this.status;
}
disconnect(): void {
this.shouldReconnect = false;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.setStatus('disconnected');
}
private scheduleReconnect(): void {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}
private setStatus(status: ConnectionStatus): void {
if (this.status !== status) {
this.status = status;
this.onStatusChange?.(status);
}
}
}