UNPKG

@synet/realtime

Version:

Realtime Communication server/client implementations

242 lines (241 loc) 9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketRealtimeServer = void 0; const ws_1 = __importStar(require("ws")); const node_crypto_1 = __importDefault(require("node:crypto")); class WebSocketRealtimeServer { constructor(options = {}, logger) { this.options = options; this.logger = logger; this.clients = new Map(); this.subscriptions = new Map(); // topic -> client ids this.startTime = Date.now(); this.eventHandlers = new Map(); this.stats = { connectedClients: 0, totalTopics: 0, messagesSent: 0, messagesReceived: 0, uptime: 0, }; } async start() { const port = this.options.transportOptions?.port || 3030; this.wsServer = new ws_1.WebSocketServer({ port }); this.wsServer.on("connection", (socket, request) => { this.handleNewConnection(socket, request); }); this.logger?.info(`Websocket server listening on port ${port}`); } async stop() { if (!this.wsServer) return; // Close all client connections const disconnectPromises = Array.from(this.clients.values()).map((client) => new Promise((resolve) => { client.close(); resolve(); })); await Promise.all(disconnectPromises); // Close server return new Promise((resolve) => { if (this.wsServer) { this.wsServer.close(() => { this.clients.clear(); this.subscriptions.clear(); resolve(); }); } else { resolve(); } }); } async broadcast(topic, event) { const clientIds = this.subscriptions.get(topic); if (!clientIds || clientIds.size === 0) { this.logger?.info(`No subscribers for topic: ${topic}`); return; } const message = JSON.stringify(event); let sentCount = 0; for (const clientId of clientIds) { const client = this.clients.get(clientId); if (client && client.readyState === ws_1.default.OPEN) { try { client.send(message); sentCount++; } catch (error) { console.error(`Failed to send to client ${clientId}:`, error); } } } this.stats.messagesSent += sentCount; this.logger?.debug(`Broadcasted to ${topic}: ${sentCount} clients received event`); } handleNewConnection(socket, request) { const clientId = node_crypto_1.default.randomUUID(); this.clients.set(clientId, socket); // Parse topic from URL path or query parameter const url = new URL(request.url || "/", `http://${request.headers.host}`); const topic = url.searchParams.get("topic") || url.pathname.split("/").filter(Boolean)[0] || "default"; this.subscribeClientToTopic(clientId, topic); this.logger?.info(`Client ${clientId} connected and subscribed to: ${topic}`); const eventData = { clientId, topic, timestamp: new Date(), metadata: { ip: request.socket.remoteAddress, userAgent: request.headers["user-agent"], }, }; // Emit with proper typing this.emit("client.connected", eventData); // Handle client disconnection socket.on("close", () => { this.handleClientDisconnection(clientId); }); // Handle client messages (minimal - just for subscribe/unsubscribe) socket.on("message", (data) => { try { const message = JSON.parse(data.toString()); if (message.type === "subscribe" && message.topic) { this.subscribeClientToTopic(clientId, message.topic); } else if (message.type === "unsubscribe" && message.topic) { this.unsubscribeClientFromTopic(clientId, message.topic); } // Note: We don't handle 'publish' here - that's for bidirectional later } catch (error) { console.error("Error processing client message:", error); } }); // Send connection confirmation socket.send(JSON.stringify({ id: node_crypto_1.default.randomUUID(), type: "connection.established", source: "server", timestamp: new Date(), data: { clientId, subscribedTopic: topic }, })); } subscribeClientToTopic(clientId, topic) { if (!this.subscriptions.has(topic)) { this.subscriptions.set(topic, new Set()); } const subscribers = this.subscriptions.get(topic); if (subscribers) { subscribers.add(clientId); } this.logger?.info(`Client ${clientId} subscribed to topic: ${topic}`); } unsubscribeClientFromTopic(clientId, topic) { const subscribers = this.subscriptions.get(topic); if (subscribers) { subscribers.delete(clientId); if (subscribers.size === 0) { this.subscriptions.delete(topic); } } this.logger?.info(`Client ${clientId} unsubscribed from topic: ${topic}`); } handleClientDisconnection(clientId) { this.logger?.info(`Client ${clientId} disconnected`); const subscribedTopics = []; for (const [topic, subscribers] of this.subscriptions.entries()) { if (subscribers.has(clientId)) { subscribedTopics.push(topic); } } this.clients.delete(clientId); // Remove from all topic subscriptions for (const [topic, subscribers] of this.subscriptions.entries()) { subscribers.delete(clientId); if (subscribers.size === 0) { this.subscriptions.delete(topic); } } // Create properly typed event data const eventData = { clientId, topics: subscribedTopics, timestamp: new Date(), }; // Emit with proper typing this.emit("client.disconnected", eventData); } getStats() { return { ...this.stats, connectedClients: this.clients.size, totalTopics: this.subscriptions.size, uptime: Date.now() - this.startTime, }; } on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, new Set()); } const handlers = this.eventHandlers.get(event); if (handlers) { handlers.add(handler); } } /** * Emit an event to all registered handlers * @param event The event type * @param data The event data */ emit(event, data) { const handlers = this.eventHandlers.get(event) || new Set(); for (const handler of handlers) { try { handler(data); } catch (error) { console.error(`Error in event handler for ${event}:`, error); } } } } exports.WebSocketRealtimeServer = WebSocketRealtimeServer;