import { IncomingMessage } from "node:http"; import { v4 as uuidv4 } from "uuid"; import { Redis, type RedisOptions } from "ioredis"; import { WebSocket, WebSocketServer, type ServerOptions } from "ws"; import { RoomManager } from "./room-manager"; import { RecordManager } from "./record-manager"; import { ConnectionManager } from "./connection-manager"; import { PresenceManager } from "./presence-manager"; import { CodeError, Status } from "../client"; import { Connection } from "./connection"; import { parseCommand, type Command } from "../common/message"; import type { Operation } from "fast-json-patch"; const PUB_SUB_CHANNEL_PREFIX = "mesh:pubsub:"; const RECORD_PUB_SUB_CHANNEL = "mesh:record-updates"; export class MeshContext { server: MeshServer; command: string; connection: Connection; payload: T; constructor( server: MeshServer, command: string, connection: Connection, payload: T ) { this.server = server; this.command = command; this.connection = connection; this.payload = payload; } } export type SocketMiddleware = ( context: MeshContext ) => any | Promise; type PubSubMessagePayload = { targetConnectionIds: string[]; command: Command; }; type RecordUpdatePubSubPayload = { recordId: string; newValue?: any; patch?: Operation[]; version: number; }; export type MeshServerOptions = ServerOptions & { /** * The interval at which to send ping messages to the client. * * @default 30000 */ pingInterval?: number; /** * The interval at which to send both latency requests and updates to the client. * * @default 5000 */ latencyInterval?: number; redisOptions: RedisOptions; /** * The maximum number of consecutive ping intervals the server will wait * for a pong response before considering the client disconnected. * A value of 1 means the client must respond within roughly 2 * pingInterval * before being disconnected. Setting it to 0 is not recommended as it will * immediately disconnect the client if it doesn't respond to the first ping in * exactly `pingInterval` milliseconds, which doesn't provide wiggle room for * network latency. * * @see pingInterval * @default 1 */ maxMissedPongs?: number; }; type ChannelPattern = string | RegExp; export class MeshServer extends WebSocketServer { readonly instanceId: string; redis: Redis; pubClient: Redis; subClient: Redis; roomManager: RoomManager; recordManager: RecordManager; connectionManager: ConnectionManager; presenceManager: PresenceManager; serverOptions: MeshServerOptions; status: Status = Status.OFFLINE; private exposedChannels: ChannelPattern[] = []; private exposedRecords: ChannelPattern[] = []; private exposedWritableRecords: ChannelPattern[] = []; // New: Track writable records private channelSubscriptions: { [channel: string]: Set } = {}; private recordSubscriptions: Map< string, // recordId Map // connectionId -> mode > = new Map(); private channelGuards: Map< ChannelPattern, (connection: Connection, channel: string) => Promise | boolean > = new Map(); private recordGuards: Map< ChannelPattern, (connection: Connection, recordId: string) => Promise | boolean > = new Map(); private writableRecordGuards: Map< // New: Guards for writable records ChannelPattern, (connection: Connection, recordId: string) => Promise | boolean > = new Map(); private _isShuttingDown = false; commands: { [command: string]: (context: MeshContext) => Promise | any; } = {}; private globalMiddlewares: SocketMiddleware[] = []; middlewares: { [key: string]: SocketMiddleware[] } = {}; private _listening = false; private _subscriptionPromise!: Promise; get listening(): boolean { return this._listening; } set listening(value: boolean) { this._listening = value; this.status = value ? Status.ONLINE : Status.OFFLINE; } constructor(opts: MeshServerOptions) { super(opts); this.instanceId = uuidv4(); this.redis = new Redis({ retryStrategy: (times: number) => { if (this._isShuttingDown) { return null; } if (times > 10) { return null; } return Math.min(1000 * Math.pow(2, times), 30000); }, ...opts.redisOptions, }); this.redis.on("error", (err) => console.error("Redis error:", err)); this.serverOptions = { ...opts, pingInterval: opts.pingInterval ?? 30_000, latencyInterval: opts.latencyInterval ?? 5_000, maxMissedPongs: opts.maxMissedPongs ?? 1, }; this.pubClient = this.redis.duplicate(); this.subClient = this.redis.duplicate(); this.roomManager = new RoomManager(this.redis); this.recordManager = new RecordManager(this.redis); this.connectionManager = new ConnectionManager( this.pubClient, this.instanceId, this.roomManager ); this.presenceManager = new PresenceManager(this.redis, this.roomManager); this.subscribeToInstanceChannel(); this.on("listening", () => { this.listening = true; }); this.on("error", (err) => { console.error(`[MeshServer] Error: ${err}`); }); this.on("close", () => { this.listening = false; }); this.registerBuiltinCommands(); this.registerRecordCommands(); // Add this line this.applyListeners(); } /** * Waits until the service is ready by ensuring it is listening and the instance channel subscription is established. * * @returns {Promise} A promise that resolves when the service is fully ready. * @throws {Error} If the readiness process fails or if any awaited promise rejects. */ async ready(): Promise { const listeningPromise = this.listening ? Promise.resolve() : new Promise((resolve) => this.once("listening", resolve)); await Promise.all([listeningPromise, this._subscriptionPromise]); } private subscribeToInstanceChannel(): Promise { const channel = `${PUB_SUB_CHANNEL_PREFIX}${this.instanceId}`; this._subscriptionPromise = new Promise((resolve, reject) => { this.subClient.subscribe(channel, RECORD_PUB_SUB_CHANNEL); this.subClient.psubscribe("mesh:presence:updates:*", (err) => { if (err) { if (!this._isShuttingDown) { console.error( `Failed to subscribe to channels ${channel}, ${RECORD_PUB_SUB_CHANNEL}:`, err ); } reject(err); return; } resolve(); }); }); this.subClient.on("message", async (channel, message) => { if (channel.startsWith(PUB_SUB_CHANNEL_PREFIX)) { this.handleInstancePubSubMessage(channel, message); } else if (channel === RECORD_PUB_SUB_CHANNEL) { this.handleRecordUpdatePubSubMessage(message); } else if (this.channelSubscriptions[channel]) { for (const connection of this.channelSubscriptions[channel]) { if (!connection.isDead) { connection.send({ command: "mesh/subscription-message", payload: { channel, message }, }); } } } }); this.subClient.on("pmessage", async (pattern, channel, message) => { if (pattern === "mesh:presence:updates:*") { // channel here is the actual channel, e.g., mesh:presence:updates:roomName const subscribers = this.channelSubscriptions[channel]; if (subscribers) { try { const payload = JSON.parse(message); subscribers.forEach((connection) => { if (!connection.isDead) { connection.send({ command: "mesh/presence-update", payload: payload, }); } else { // clean up dead connections from subscription list subscribers.delete(connection); } }); } catch (e) { this.emit( "error", new Error(`Failed to parse presence update: ${message}`) ); } } } }); return this._subscriptionPromise; } private handleInstancePubSubMessage(channel: string, message: string) { try { const parsedMessage = JSON.parse(message) as PubSubMessagePayload; if ( !parsedMessage || !Array.isArray(parsedMessage.targetConnectionIds) || !parsedMessage.command || typeof parsedMessage.command.command !== "string" ) { throw new Error("Invalid message format"); } const { targetConnectionIds, command } = parsedMessage; targetConnectionIds.forEach((connectionId) => { const connection = this.connectionManager.getLocalConnection(connectionId); if (connection && !connection.isDead) { connection.send(command); } }); } catch (err) { this.emit("error", new Error(`Failed to parse message: ${message}`)); } } private handleRecordUpdatePubSubMessage(message: string) { try { const parsedMessage = JSON.parse(message) as RecordUpdatePubSubPayload; const { recordId, newValue, patch, version } = parsedMessage; if (!recordId || typeof version !== "number") { throw new Error("Invalid record update message format"); } const subscribers = this.recordSubscriptions.get(recordId); if (!subscribers) { return; } subscribers.forEach((mode, connectionId) => { const connection = this.connectionManager.getLocalConnection(connectionId); if (connection && !connection.isDead) { if (mode === "patch" && patch) { connection.send({ command: "mesh/record-update", payload: { recordId, patch, version }, }); } else if (mode === "full" && newValue !== undefined) { connection.send({ command: "mesh/record-update", payload: { recordId, full: newValue, version }, }); } } else if (!connection) { subscribers.delete(connectionId); if (subscribers.size === 0) { this.recordSubscriptions.delete(recordId); } } }); } catch (err) { this.emit( "error", new Error(`Failed to parse record update message: ${message}`) ); } } private applyListeners() { this.on("connection", async (socket: WebSocket, req: IncomingMessage) => { const connection = new Connection(socket, req, this.serverOptions); connection.on("message", (buffer: Buffer) => { try { const data = buffer.toString(); const command = parseCommand(data); if ( command.id !== undefined && !["latency:response", "pong"].includes(command.command) ) { this.runCommand( command.id, command.command, command.payload, connection ); } } catch (err) { this.emit("error", err); } }); try { await this.connectionManager.registerConnection(connection); } catch (error) { connection.close(); return; } this.emit("connected", connection); connection.on("close", async () => { await this.cleanupConnection(connection); this.emit("disconnected", connection); }); connection.on("error", (err) => { this.emit("clientError", err, connection); }); connection.on("pong", async (connectionId) => { try { const rooms = await this.roomManager.getRoomsForConnection( connectionId ); for (const roomName of rooms) { if (await this.presenceManager.isRoomTracked(roomName)) { await this.presenceManager.refreshPresence( connectionId, roomName ); } } } catch (err) { this.emit("error", new Error(`Failed to refresh presence: ${err}`)); } }); }); } /** * Exposes a channel for external access and optionally associates a guard function * to control access to that channel. The guard function determines whether a given * connection is permitted to access the channel. * * @param {ChannelPattern} channel - The channel or pattern to expose. * @param {(connection: Connection, channel: string) => Promise | boolean} [guard] - * Optional guard function that receives the connection and channel name, returning * a boolean or a promise that resolves to a boolean indicating whether access is allowed. * @returns {void} */ exposeChannel( channel: ChannelPattern, guard?: ( connection: Connection, channel: string ) => Promise | boolean ): void { this.exposedChannels.push(channel); if (guard) { this.channelGuards.set(channel, guard); } } private async isChannelExposed( channel: string, connection: Connection ): Promise { const matchedPattern = this.exposedChannels.find((pattern) => typeof pattern === "string" ? pattern === channel : pattern.test(channel) ); if (!matchedPattern) { return false; } const guard = this.channelGuards.get(matchedPattern); if (guard) { try { return await Promise.resolve(guard(connection, channel)); } catch (e) { return false; } } return true; } /** * Exposes a record or pattern for client subscriptions, optionally adding a guard function. * * @param {ChannelPattern} recordPattern - The record ID or pattern to expose. * @param {(connection: Connection, recordId: string) => Promise | boolean} [guard] - Optional guard function. */ exposeRecord( recordPattern: ChannelPattern, guard?: ( connection: Connection, recordId: string ) => Promise | boolean ): void { this.exposedRecords.push(recordPattern); if (guard) { this.recordGuards.set(recordPattern, guard); } } private async isRecordExposed( recordId: string, connection: Connection ): Promise { const readPattern = this.exposedRecords.find((pattern) => typeof pattern === "string" ? pattern === recordId : pattern.test(recordId) ); let canRead = false; if (readPattern) { const guard = this.recordGuards.get(readPattern); if (guard) { try { canRead = await Promise.resolve(guard(connection, recordId)); } catch (e) { canRead = false; } } else { canRead = true; } } if (canRead) { return true; } // if exposed as writable, it is implicitly readable const writePattern = this.exposedWritableRecords.find((pattern) => typeof pattern === "string" ? pattern === recordId : pattern.test(recordId) ); // If exposed as writable, it's readable. No need to check the *write* guard here. if (writePattern) { return true; } return false; } /** * Exposes a record or pattern for client writes, optionally adding a guard function. * * @param {ChannelPattern} recordPattern - The record ID or pattern to expose as writable. * @param {(connection: Connection, recordId: string) => Promise | boolean} [guard] - Optional guard function. */ exposeWritableRecord( recordPattern: ChannelPattern, guard?: ( connection: Connection, recordId: string ) => Promise | boolean ): void { this.exposedWritableRecords.push(recordPattern); if (guard) { this.writableRecordGuards.set(recordPattern, guard); } } private async isRecordWritable( recordId: string, connection: Connection ): Promise { const matchedPattern = this.exposedWritableRecords.find((pattern) => typeof pattern === "string" ? pattern === recordId : pattern.test(recordId) ); if (!matchedPattern) { return false; } const guard = this.writableRecordGuards.get(matchedPattern); if (guard) { try { return await Promise.resolve(guard(connection, recordId)); } catch (e) { return false; } } return true; } /** * Publishes a message to a specified channel and optionally maintains a history of messages. * * @param {string} channel - The name of the channel to which the message will be published. * @param {any} message - The message to be published. Will not be stringified automatically for you. You need to do that yourself. * @param {number} [history=0] - The number of historical messages to retain for the channel. Defaults to 0, meaning no history is retained. * If greater than 0, the message will be added to the channel's history and the history will be trimmed to the specified size. * @returns {Promise} A Promise that resolves once the message has been published and, if applicable, the history has been updated. * @throws {Error} This function may throw an error if the underlying `pubClient` operations (e.g., `lpush`, `ltrim`, `publish`) fail. */ async publishToChannel( channel: string, message: any, history: number = 0 ): Promise { const parsedHistory = parseInt(history as any, 10); if (!isNaN(parsedHistory) && parsedHistory > 0) { await this.pubClient.lpush(`history:${channel}`, message); await this.pubClient.ltrim(`history:${channel}`, 0, parsedHistory); } await this.pubClient.publish(channel, message); } /** * Updates a record, persists it to Redis, increments its version, computes a patch, * and publishes the update via Redis pub/sub. * * @param {string} recordId - The ID of the record to update. * @param {any} newValue - The new value for the record. * @returns {Promise} * @throws {Error} If the update fails. */ async publishRecordUpdate(recordId: string, newValue: any): Promise { const updateResult = await this.recordManager.publishUpdate( recordId, newValue ); if (!updateResult) { return; } const { patch, version } = updateResult; const messagePayload: RecordUpdatePubSubPayload = { recordId, newValue, patch, version, }; try { await this.pubClient.publish( RECORD_PUB_SUB_CHANNEL, JSON.stringify(messagePayload) ); } catch (err) { this.emit( "error", new Error(`Failed to publish record update for "${recordId}": ${err}`) ); } } /** * Adds one or more middleware functions to the global middleware stack. * * @param {SocketMiddleware[]} middlewares - An array of middleware functions to be added. Each middleware * is expected to conform to the `SocketMiddleware` type. * @returns {void} * @throws {Error} If the provided middlewares are not valid or fail validation (if applicable). */ addMiddleware(...middlewares: SocketMiddleware[]): void { this.globalMiddlewares.push(...middlewares); } /** * Registers a command with an associated callback and optional middleware. * * @template T The type for `MeshContext.payload`. Defaults to `any`. * @template U The command's return value type. Defaults to `any`. * @param {string} command - The unique identifier for the command to register. * @param {(context: MeshContext) => Promise | U} callback - The function to execute when the command is invoked. It receives a `MeshContext` of type `T` and may return a value of type `U` or a `Promise` resolving to `U`. * @param {SocketMiddleware[]} [middlewares=[]] - An optional array of middleware functions to apply to the command. Defaults to an empty array. * @throws {Error} May throw an error if the command registration or middleware addition fails. */ registerCommand( command: string, callback: (context: MeshContext) => Promise | U, middlewares: SocketMiddleware[] = [] ) { this.commands[command] = callback; if (middlewares.length > 0) { this.addMiddlewareToCommand(command, middlewares); } } trackPresence( roomPattern: string | RegExp, guardOrOptions?: | (( connection: Connection, roomName: string ) => Promise | boolean) | { ttl?: number; guard?: ( connection: Connection, roomName: string ) => Promise | boolean; } ): void { this.presenceManager.trackRoom(roomPattern, guardOrOptions); } private registerBuiltinCommands() { this.registerCommand< { channel: string; historyLimit?: number }, { success: boolean; history?: string[] } >("mesh/subscribe-channel", async (ctx) => { const { channel, historyLimit } = ctx.payload; if (!(await this.isChannelExposed(channel, ctx.connection))) { return { success: false, history: [] }; } try { if (!this.channelSubscriptions[channel]) { this.channelSubscriptions[channel] = new Set(); await new Promise((resolve, reject) => { this.subClient.subscribe(channel, (err) => { if (err) reject(err); else resolve(); }); }); } this.channelSubscriptions[channel].add(ctx.connection); let history: string[] = []; if (historyLimit && historyLimit > 0) { const historyKey = `history:${channel}`; history = await this.redis.lrange(historyKey, 0, historyLimit - 1); } return { success: true, history, }; } catch (e) { return { success: false, history: [] }; } }); this.registerCommand<{ channel: string }, boolean>( "mesh/unsubscribe-channel", async (ctx) => { const { channel } = ctx.payload; if (this.channelSubscriptions[channel]) { this.channelSubscriptions[channel].delete(ctx.connection); if (this.channelSubscriptions[channel].size === 0) { await new Promise((resolve, reject) => { this.subClient.unsubscribe(channel, (err) => { if (err) reject(err); else resolve(); }); }); delete this.channelSubscriptions[channel]; } return true; } return false; } ); } private registerRecordCommands() { this.registerCommand< { recordId: string; mode?: "patch" | "full" }, { success: boolean; record?: any; version?: number } >("mesh/subscribe-record", async (ctx) => { const { recordId, mode = "full" } = ctx.payload; const connectionId = ctx.connection.id; if (!(await this.isRecordExposed(recordId, ctx.connection))) { return { success: false }; } try { const { record, version } = await this.recordManager.getRecordAndVersion(recordId); if (!this.recordSubscriptions.has(recordId)) { this.recordSubscriptions.set(recordId, new Map()); } this.recordSubscriptions.get(recordId)!.set(connectionId, mode); return { success: true, record, version }; } catch (e) { console.error(`Failed to subscribe to record ${recordId}:`, e); return { success: false }; } }); this.registerCommand<{ recordId: string }, boolean>( "mesh/unsubscribe-record", async (ctx) => { const { recordId } = ctx.payload; const connectionId = ctx.connection.id; const recordSubs = this.recordSubscriptions.get(recordId); if (recordSubs?.has(connectionId)) { recordSubs.delete(connectionId); if (recordSubs.size === 0) { this.recordSubscriptions.delete(recordId); } return true; } return false; } ); // New command for client-initiated record updates this.registerCommand< { recordId: string; newValue: any }, { success: boolean } >("mesh/publish-record-update", async (ctx) => { const { recordId, newValue } = ctx.payload; if (!(await this.isRecordWritable(recordId, ctx.connection))) { throw new CodeError( `Record "${recordId}" is not writable by this connection.`, "EACCESS", "PermissionError" ); } try { await this.publishRecordUpdate(recordId, newValue); return { success: true }; } catch (e: any) { throw new CodeError( `Failed to publish update for record "${recordId}": ${e.message}`, "EUPDATE", "UpdateError" ); } }); this.registerCommand< { roomName: string }, { success: boolean; present: string[] } >("mesh/subscribe-presence", async (ctx) => { const { roomName } = ctx.payload; const connectionId = ctx.connection.id; if ( !(await this.presenceManager.isRoomTracked(roomName, ctx.connection)) ) { return { success: false, present: [] }; } try { const presenceChannel = `mesh:presence:updates:${roomName}`; if (!this.channelSubscriptions[presenceChannel]) { this.channelSubscriptions[presenceChannel] = new Set(); } this.channelSubscriptions[presenceChannel].add(ctx.connection); const present = await this.presenceManager.getPresentConnections( roomName ); return { success: true, present }; } catch (e) { console.error( `Failed to subscribe to presence for room ${roomName}:`, e ); return { success: false, present: [] }; } }); this.registerCommand<{ roomName: string }, boolean>( "mesh/unsubscribe-presence", async (ctx) => { const { roomName } = ctx.payload; const presenceChannel = `mesh:presence:updates:${roomName}`; if (this.channelSubscriptions[presenceChannel]) { this.channelSubscriptions[presenceChannel].delete(ctx.connection); if (this.channelSubscriptions[presenceChannel].size === 0) { delete this.channelSubscriptions[presenceChannel]; } return true; } return false; } ); } /** * Adds an array of middleware functions to a specific command. * * @param {string} command - The name of the command to associate the middleware with. * @param {SocketMiddleware[]} middlewares - An array of middleware functions to be added to the command. * @returns {void} */ addMiddlewareToCommand( command: string, middlewares: SocketMiddleware[] ): void { if (middlewares.length) { this.middlewares[command] = this.middlewares[command] || []; this.middlewares[command] = middlewares.concat(this.middlewares[command]); } } private async cleanupRecordSubscriptions(connection: Connection) { const connectionId = connection.id; this.recordSubscriptions.forEach((subscribers, recordId) => { if (subscribers.has(connectionId)) { subscribers.delete(connectionId); if (subscribers.size === 0) { this.recordSubscriptions.delete(recordId); } } }); } private async runCommand( id: number, commandName: string, payload: any, connection: Connection ) { const context = new MeshContext(this, commandName, connection, payload); try { if (!this.commands[commandName]) { throw new CodeError( `Command "${commandName}" not found`, "ENOTFOUND", "CommandError" ); } if (this.globalMiddlewares.length) { for (const middleware of this.globalMiddlewares) { await middleware(context); } } if (this.middlewares[commandName]) { for (const middleware of this.middlewares[commandName]) { await middleware(context); } } const result = await this.commands[commandName](context); connection.send({ id, command: commandName, payload: result }); } catch (err) { const errorPayload = err instanceof Error ? { error: err.message, code: (err as CodeError).code || "ESERVER", name: err.name || "Error", } : { error: String(err), code: "EUNKNOWN", name: "UnknownError" }; connection.send({ id, command: commandName, payload: errorPayload }); } } /** * Broadcasts a command and payload to a set of connections or all available connections. * * @param {string} command - The command to be broadcasted. * @param {any} payload - The data associated with the command. * @param {Connection[]=} connections - (Optional) A specific list of connections to broadcast to. If not provided, the command will be sent to all connections. * * @throws {Error} Emits an "error" event if broadcasting fails. */ async broadcast(command: string, payload: any, connections?: Connection[]) { const cmd: Command = { command, payload }; try { if (connections) { const allConnectionIds = connections.map(({ id }) => id); const connectionIds = await this.connectionManager.getAllConnectionIds(); const filteredIds = allConnectionIds.filter((id) => connectionIds.includes(id) ); await this.publishOrSend(filteredIds, cmd); } else { const allConnectionIds = await this.connectionManager.getAllConnectionIds(); await this.publishOrSend(allConnectionIds, cmd); } } catch (err) { this.emit( "error", new Error(`Failed to broadcast command "${command}": ${err}`) ); } } /** * Broadcasts a command and associated payload to all active connections within the specified room. * * @param {string} roomName - The name of the room whose connections will receive the broadcast. * @param {string} command - The command to be broadcasted to the connections. * @param {unknown} payload - The data payload associated with the command. * @returns {Promise} A promise that resolves when the broadcast operation is complete. * @throws {Error} If the broadcast operation fails, an error is thrown and the promise is rejected. */ async broadcastRoom( roomName: string, command: string, payload: any ): Promise { const connectionIds = await this.roomManager.getRoomConnectionIds(roomName); try { await this.publishOrSend(connectionIds, { command, payload }); } catch (err) { this.emit( "error", new Error(`Failed to broadcast command "${command}": ${err}`) ); } } /** * Broadcasts a command and payload to all active connections except for the specified one(s). * Excludes the provided connection(s) from receiving the broadcast. * * @param {string} command - The command to broadcast to connections. * @param {any} payload - The payload to send along with the command. * @param {Connection | Connection[]} exclude - A single connection or an array of connections to exclude from the broadcast. * @returns {Promise} A promise that resolves when the broadcast is complete. * @emits {Error} Emits an "error" event if broadcasting the command fails. */ async broadcastExclude( command: string, payload: any, exclude: Connection | Connection[] ): Promise { const excludedIds = new Set( (Array.isArray(exclude) ? exclude : [exclude]).map(({ id }) => id) ); try { const connectionIds = ( await this.connectionManager.getAllConnectionIds() ).filter((id) => !excludedIds.has(id)); await this.publishOrSend(connectionIds, { command, payload }); } catch (err) { this.emit( "error", new Error(`Failed to broadcast command "${command}": ${err}`) ); } } /** * Broadcasts a command with a payload to all connections in a specified room, * excluding one or more given connections. If the broadcast fails, emits an error event. * * @param {string} roomName - The name of the room to broadcast to. * @param {string} command - The command to broadcast. * @param {any} payload - The payload to send with the command. * @param {Connection | Connection[]} exclude - A connection or array of connections to exclude from the broadcast. * @returns {Promise} A promise that resolves when the broadcast is complete. * @emits {Error} Emits an error event if broadcasting fails. */ async broadcastRoomExclude( roomName: string, command: string, payload: any, exclude: Connection | Connection[] ): Promise { const excludedIds = new Set( (Array.isArray(exclude) ? exclude : [exclude]).map(({ id }) => id) ); try { const connectionIds = ( await this.roomManager.getRoomConnectionIds(roomName) ).filter((id) => !excludedIds.has(id)); await this.publishOrSend(connectionIds, { command, payload }); } catch (err) { this.emit( "error", new Error(`Failed to broadcast command "${command}": ${err}`) ); } } private async publishOrSend(connectionIds: string[], command: Command) { if (connectionIds.length === 0) { return; } // get instance mapping for the target connection IDs const connectionInstanceMapping = await this.connectionManager.getInstanceIdsForConnections(connectionIds); const instanceMap: { [instanceId: string]: string[] } = {}; // group connection IDs by instance ID for (const connectionId of connectionIds) { const instanceId = connectionInstanceMapping[connectionId]; if (instanceId) { if (!instanceMap[instanceId]) { instanceMap[instanceId] = []; } instanceMap[instanceId].push(connectionId); } } // publish command to each instance for (const [instanceId, targetConnectionIds] of Object.entries( instanceMap )) { if (targetConnectionIds.length === 0) continue; if (instanceId === this.instanceId) { // send locally targetConnectionIds.forEach((connectionId) => { const connection = this.connectionManager.getLocalConnection(connectionId); if (connection && !connection.isDead) { connection.send(command); } }); } else { // publish to remote instance via pubsub const messagePayload: PubSubMessagePayload = { targetConnectionIds, command, }; const message = JSON.stringify(messagePayload); try { await this.pubClient.publish( this.getPubSubChannel(instanceId), message ); } catch (err) { this.emit( "error", new Error(`Failed to publish command "${command.command}": ${err}`) ); } } } } async isInRoom(roomName: string, connection: Connection | string) { const connectionId = typeof connection === "string" ? connection : connection.id; return this.roomManager.connectionIsInRoom(roomName, connectionId); } async addToRoom(roomName: string, connection: Connection | string) { const connectionId = typeof connection === "string" ? connection : connection.id; await this.roomManager.addToRoom(roomName, connection); if (await this.presenceManager.isRoomTracked(roomName)) { await this.presenceManager.markOnline(connectionId, roomName); } } async removeFromRoom(roomName: string, connection: Connection | string) { const connectionId = typeof connection === "string" ? connection : connection.id; if (await this.presenceManager.isRoomTracked(roomName)) { await this.presenceManager.markOffline(connectionId, roomName); } return this.roomManager.removeFromRoom(roomName, connection); } async removeFromAllRooms(connection: Connection | string) { return this.roomManager.removeFromAllRooms(connection); } async clearRoom(roomName: string) { return this.roomManager.clearRoom(roomName); } async getRoomMembers(roomName: string): Promise { return this.roomManager.getRoomConnectionIds(roomName); } private getPubSubChannel(instanceId: string): string { return `${PUB_SUB_CHANNEL_PREFIX}${instanceId}`; } private async cleanupConnection(connection: Connection) { connection.stopIntervals(); try { await this.presenceManager.cleanupConnection(connection); await this.connectionManager.cleanupConnection(connection); await this.roomManager.cleanupConnection(connection); await this.cleanupRecordSubscriptions(connection); } catch (err) { this.emit("error", new Error(`Failed to clean up connection: ${err}`)); } } /** * Gracefully closes all active connections, cleans up resources, * and shuts down the service. Optionally accepts a callback function * that will be invoked once shutdown is complete or if an error occurs. * * @param {((err?: Error) => void)=} callback - Optional callback to be invoked when closing is complete or if an error occurs. * @returns {Promise} A promise that resolves when shutdown is complete. * @throws {Error} If an error occurs during shutdown, the promise will be rejected with the error. */ async close(callback?: (err?: Error) => void): Promise { this._isShuttingDown = true; const connections = Object.values( this.connectionManager.getLocalConnections() ); await Promise.all( connections.map(async (connection) => { if (!connection.isDead) { await connection.close(); } await this.cleanupConnection(connection); }) ); await new Promise((resolve, reject) => { super.close((err?: Error) => { if (err) reject(err); else resolve(); }); }); this.pubClient.disconnect(); this.subClient.disconnect(); this.redis.disconnect(); this.listening = false; this.removeAllListeners(); if (callback) { callback(); } } /** * Registers a callback function to be executed when a new connection is established. * * @param {(connection: Connection) => Promise | void} callback - The function to execute when a new connection is established. * @returns {MeshServer} The server instance for method chaining. */ onConnection( callback: (connection: Connection) => Promise | void ): MeshServer { this.on("connected", callback); return this; } /** * Registers a callback function to be executed when a connection is closed. * * @param {(connection: Connection) => Promise | void} callback - The function to execute when a connection is closed. * @returns {MeshServer} The server instance for method chaining. */ onDisconnection( callback: (connection: Connection) => Promise | void ): MeshServer { this.on("disconnected", callback); return this; } }