UNPKG

@synet/realtime

Version:

Realtime Communication server/client implementations

220 lines (218 loc) 8.33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketRealtimeChannel = void 0; const realtime_1 = require("@synet/patterns/realtime"); const node_crypto_1 = __importDefault(require("node:crypto")); // Add this import /** * WebSocket implementation of RealtimeChannel */ class WebSocketRealtimeChannel { constructor(url, options = {}) { this.url = url; this.options = options; this.id = node_crypto_1.default.randomUUID(); this.socket = null; this.state = realtime_1.ChannelState.DISCONNECTED; this.eventHandlers = new Map(); this.reconnectTimer = null; this.reconnectAttempts = 0; this.connectionPromise = null; if (this.options.authToken) { const urlObj = new URL(this.url); urlObj.searchParams.set("token", this.options.authToken); this.url = urlObj.toString(); } } /** * Connect to the WebSocket channel * Returns immediately but establishes connection in background */ connect() { // If already connecting or connected, return the existing promise if (this.connectionPromise) { return this.connectionPromise; } if (this.state === realtime_1.ChannelState.CONNECTED) { return Promise.resolve(); } this.state = realtime_1.ChannelState.CONNECTING; this.connectionPromise = new Promise((resolve, reject) => { try { this.socket = new WebSocket(this.url); this.socket.onopen = () => { this.state = realtime_1.ChannelState.CONNECTED; this.reconnectAttempts = 0; resolve(); }; this.socket.onclose = (event) => { const previousState = this.state; this.state = realtime_1.ChannelState.DISCONNECTED; this.connectionPromise = null; if (previousState === realtime_1.ChannelState.CONNECTED && this.options.reconnect?.enabled) { this.scheduleReconnect(); } if (previousState === realtime_1.ChannelState.CONNECTING) { reject(new Error(`Failed to connect: ${event.code}`)); } }; this.socket.onerror = (error) => { const previousState = this.state; this.state = realtime_1.ChannelState.ERROR; if (previousState === realtime_1.ChannelState.CONNECTING) { this.connectionPromise = null; reject(error); } }; this.socket.onmessage = (message) => { try { const event = JSON.parse(message.data); this.processIncomingEvent(event); } catch (error) { console.error("Error processing message:", error); } }; } catch (error) { this.state = realtime_1.ChannelState.ERROR; this.connectionPromise = null; reject(error); } }); // Connection happens in background, we don't await it here return this.connectionPromise; } on(selector, handler) { let eventType; if (typeof selector === "string") { eventType = selector; } else if (selector.type && selector.source) { eventType = `${selector.source}.${selector.type}`; } else if (selector.type) { eventType = selector.type; } else if (selector.source) { eventType = `${selector.source}.*`; } else { eventType = "*"; } if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, new Set()); } const handlers = this.eventHandlers.get(eventType); if (handlers) { handlers.add(handler); } return () => { const handlers = this.eventHandlers.get(eventType); if (handlers) { handlers.delete(handler); if (handlers.size === 0) { this.eventHandlers.delete(eventType); } } }; } /* async emit(RealtimeEvent: TOut): Promise<void> { if (this.state !== ChannelState.CONNECTED || !this.socket) { throw new Error(`Cannot emit RealtimeEvent: channel not connected (state: ${this.state})`); } this.socket.send(JSON.stringify(RealtimeEvent)); } */ async emit(event) { // If connecting, wait for the connection to complete if (this.state === realtime_1.ChannelState.CONNECTING && this.connectionPromise) { await this.connectionPromise; } // If disconnected or error, try to establish connection else if (this.state !== realtime_1.ChannelState.CONNECTED) { await this.connect(); } // At this point we should be connected, but double-check if (!this.socket || this.state !== realtime_1.ChannelState.CONNECTED) { throw new Error(`Cannot emit event: channel not connected (state: ${this.state})`); } this.socket.send(JSON.stringify(event)); } async close() { if (this.state === realtime_1.ChannelState.DISCONNECTED || !this.socket) { return; } this.state = realtime_1.ChannelState.DISCONNECTING; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } return new Promise((resolve) => { if (!this.socket) { this.state = realtime_1.ChannelState.DISCONNECTED; resolve(); return; } this.socket.onclose = () => { this.state = realtime_1.ChannelState.DISCONNECTED; this.socket = null; resolve(); }; this.socket.close(); }); } getState() { return this.state; } processIncomingEvent(RealtimeEvent) { // Process exact type handlers const exactHandlers = this.eventHandlers.get(RealtimeEvent.type); if (exactHandlers) { for (const handler of exactHandlers) { try { handler(RealtimeEvent); } catch (error) { console.error(`Error in RealtimeEvent handler for ${RealtimeEvent.type}:`, error); } } } // Process wildcard handlers const wildcardHandlers = this.eventHandlers.get("*"); if (wildcardHandlers) { for (const handler of wildcardHandlers) { try { handler(RealtimeEvent); } catch (error) { console.error(`Error in wildcard handler for ${RealtimeEvent.type}:`, error); } } } } scheduleReconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } const maxAttempts = this.options.reconnect?.maxAttempts ?? 5; if (this.reconnectAttempts >= maxAttempts) { return; } const initialDelay = this.options.reconnect?.initialDelayMs ?? 1000; const maxDelay = this.options.reconnect?.maxDelayMs ?? 30000; // Exponential backoff const delay = Math.min(initialDelay * 2 ** this.reconnectAttempts, maxDelay); this.reconnectAttempts++; this.reconnectTimer = setTimeout(() => { this.connect().catch((error) => { console.error("Reconnection failed:", error); this.scheduleReconnect(); }); }, delay); } } exports.WebSocketRealtimeChannel = WebSocketRealtimeChannel;