@synet/realtime
Version:
Realtime Communication server/client implementations
242 lines (241 loc) • 9 kB
JavaScript
"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;