Add browser support with Socket.io transport, authentication, performance-optimized IPC, and CI/CD (#925)
This commit is contained in:
29
events/EventSystem/BrowserEventBus.ts
Normal file
29
events/EventSystem/BrowserEventBus.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Browser-specific EventBus implementation using Socket.io
|
||||
import io from 'socket.io-client'
|
||||
import { SocketIOClientEventBus } from './SocketIOClientEventBus'
|
||||
import { Rpc } from './Rpc'
|
||||
|
||||
// Get auth from sessionStorage or use empty (will show login dialog)
|
||||
const username = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-username') || '' : ''
|
||||
const password = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-password') || '' : ''
|
||||
|
||||
// Connect to the server (same origin in browser mode)
|
||||
const socket = io({
|
||||
auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: Infinity,
|
||||
transports: ['websocket', 'polling'],
|
||||
})
|
||||
|
||||
export const rendererEvents = new SocketIOClientEventBus(socket)
|
||||
export const rendererRpc = new Rpc(rendererEvents)
|
||||
|
||||
// In browser mode, the backend is on the server
|
||||
// For compatibility, export same instances (renderer communicates with server backend via socket)
|
||||
export const backendEvents = rendererEvents
|
||||
export const backendRpc = rendererRpc
|
||||
@@ -1,24 +1,54 @@
|
||||
import { IpcMain } from 'electron'
|
||||
import { IpcMain, WebContents } from 'electron'
|
||||
import { Event } from '../Events'
|
||||
import { EventBusInterface } from './EventBusInterface'
|
||||
|
||||
export class IpcMainEventBus implements EventBusInterface {
|
||||
private ipc: IpcMain
|
||||
private client: any
|
||||
private clients: Map<number, WebContents> = new Map() // webContentsId -> WebContents
|
||||
private connectionOwners: Map<string, number> = new Map() // connectionId -> webContentsId
|
||||
private currentClient: WebContents | undefined
|
||||
|
||||
constructor(ipc: IpcMain) {
|
||||
this.ipc = ipc
|
||||
}
|
||||
|
||||
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||
console.log('subscribing', subscribeEvent.topic)
|
||||
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
|
||||
this.client = event.sender
|
||||
const sender = event.sender as WebContents
|
||||
this.currentClient = sender
|
||||
|
||||
// Track the client (O(1) operation)
|
||||
if (!this.clients.has(sender.id)) {
|
||||
this.clients.set(sender.id, sender)
|
||||
|
||||
// Clean up when window is closed
|
||||
sender.once('destroyed', () => {
|
||||
this.clients.delete(sender.id)
|
||||
|
||||
// Clean up owned connections
|
||||
for (const [connectionId, webContentsId] of this.connectionOwners.entries()) {
|
||||
if (webContentsId === sender.id) {
|
||||
this.connectionOwners.delete(connectionId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Track connection ownership
|
||||
if (subscribeEvent.topic === 'connection/add/mqtt' && arg?.id) {
|
||||
this.connectionOwners.set(arg.id, sender.id)
|
||||
}
|
||||
|
||||
// Remove connection ownership
|
||||
if (subscribeEvent.topic === 'connection/remove' && typeof arg === 'string') {
|
||||
this.connectionOwners.delete(arg)
|
||||
}
|
||||
|
||||
callback(arg)
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||
console.log('unsubscribeAll', event.topic)
|
||||
this.ipc.removeAllListeners(event.topic)
|
||||
}
|
||||
|
||||
@@ -27,8 +57,44 @@ export class IpcMainEventBus implements EventBusInterface {
|
||||
}
|
||||
|
||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||
if (!this.client.isDestroyed()) {
|
||||
this.client.send(event.topic, msg)
|
||||
const topic = event.topic
|
||||
|
||||
// RPC responses go only to the requesting client
|
||||
if (topic.includes('/response/')) {
|
||||
if (this.currentClient && !this.currentClient.isDestroyed()) {
|
||||
this.currentClient.send(topic, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Connection-specific events - optimized with early pattern match
|
||||
if (topic.startsWith('conn/')) {
|
||||
const parts = topic.split('/')
|
||||
let connectionId: string | undefined
|
||||
|
||||
if (parts.length === 2) {
|
||||
connectionId = parts[1]
|
||||
} else if (parts.length === 3 && (parts[1] === 'state' || parts[1] === 'publish')) {
|
||||
connectionId = parts[2]
|
||||
}
|
||||
|
||||
if (connectionId) {
|
||||
const ownerWebContentsId = this.connectionOwners.get(connectionId)
|
||||
if (ownerWebContentsId !== undefined) {
|
||||
const ownerClient = this.clients.get(ownerWebContentsId)
|
||||
if (ownerClient && !ownerClient.isDestroyed()) {
|
||||
ownerClient.send(topic, msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All other events go to all clients
|
||||
this.clients.forEach(client => {
|
||||
if (!client.isDestroyed()) {
|
||||
client.send(topic, msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
61
events/EventSystem/IpcMainEventBusV2.ts
Normal file
61
events/EventSystem/IpcMainEventBusV2.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { IpcMain } from 'electron'
|
||||
import { Event } from '../Events'
|
||||
import { EventBusInterface } from './EventBusInterface'
|
||||
import { MessageCodec } from './MessageCodec'
|
||||
|
||||
/**
|
||||
* Enhanced IPC Main Event Bus with Protobuf support
|
||||
*
|
||||
* This version uses binary serialization for better performance
|
||||
* while maintaining backward compatibility with the old JSON-based system.
|
||||
*/
|
||||
export class IpcMainEventBusV2 implements EventBusInterface {
|
||||
private ipc: IpcMain
|
||||
private client: any
|
||||
private useBinary: boolean
|
||||
|
||||
constructor(ipc: IpcMain, useBinary: boolean = true) {
|
||||
this.ipc = ipc
|
||||
this.useBinary = useBinary
|
||||
}
|
||||
|
||||
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||
console.log('subscribing', subscribeEvent.topic, this.useBinary ? '(binary)' : '(json)')
|
||||
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
|
||||
this.client = event.sender
|
||||
|
||||
if (this.useBinary && arg instanceof Uint8Array) {
|
||||
// Binary message - decode it
|
||||
const { data } = MessageCodec.decodeWithPayload<MessageType>(arg)
|
||||
callback(data)
|
||||
} else {
|
||||
// Regular JSON message
|
||||
callback(arg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||
console.log('unsubscribeAll', event.topic)
|
||||
this.ipc.removeAllListeners(event.topic)
|
||||
}
|
||||
|
||||
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||
throw new Error('Not implemented') // Todo: implement
|
||||
}
|
||||
|
||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||
if (!this.client || this.client.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.useBinary) {
|
||||
// Encode as binary
|
||||
const binary = MessageCodec.encode(event.topic, msg)
|
||||
this.client.send(event.topic, binary)
|
||||
} else {
|
||||
// Send as JSON (legacy)
|
||||
this.client.send(event.topic, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
events/EventSystem/IpcRendererEventBusV2.ts
Normal file
65
events/EventSystem/IpcRendererEventBusV2.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { CallbackStore } from './CallbackStore'
|
||||
import { EventBusInterface } from './EventBusInterface'
|
||||
import { Event } from '../Events'
|
||||
import { IpcRenderer } from 'electron'
|
||||
import { MessageCodec } from './MessageCodec'
|
||||
|
||||
/**
|
||||
* Enhanced IPC Renderer Event Bus with Protobuf support
|
||||
*
|
||||
* This version uses binary serialization for better performance
|
||||
* while maintaining backward compatibility with the old JSON-based system.
|
||||
*/
|
||||
export class IpcRendererEventBusV2 implements EventBusInterface {
|
||||
private ipc: IpcRenderer
|
||||
private callbacks: Array<CallbackStore> = []
|
||||
private useBinary: boolean
|
||||
|
||||
constructor(ipc: IpcRenderer, useBinary: boolean = true) {
|
||||
this.ipc = ipc
|
||||
this.useBinary = useBinary
|
||||
}
|
||||
|
||||
public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||
const wrappedCallback = (_: any, arg: any) => {
|
||||
if (this.useBinary && arg instanceof Uint8Array) {
|
||||
// Binary message - decode it
|
||||
const { data } = MessageCodec.decodeWithPayload<MessageType>(arg)
|
||||
callback(data)
|
||||
} else {
|
||||
// Regular JSON message
|
||||
callback(arg)
|
||||
}
|
||||
}
|
||||
console.log('subscribing', event.topic, this.useBinary ? '(binary)' : '(json)')
|
||||
this.ipc.on(event.topic, wrappedCallback)
|
||||
this.callbacks.push({
|
||||
callback,
|
||||
wrappedCallback,
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||
this.ipc.removeAllListeners(event.topic)
|
||||
}
|
||||
|
||||
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||
const item = this.callbacks.find(store => store.callback === callback)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
this.ipc.removeListener(event.topic, item.wrappedCallback)
|
||||
this.callbacks = this.callbacks.filter(a => a !== item)
|
||||
}
|
||||
|
||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||
if (this.useBinary) {
|
||||
// Encode as binary
|
||||
const binary = MessageCodec.encode(event.topic, msg)
|
||||
this.ipc.send(event.topic, binary)
|
||||
} else {
|
||||
// Send as JSON (legacy)
|
||||
this.ipc.send(event.topic, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
74
events/EventSystem/MessageCodec.ts
Normal file
74
events/EventSystem/MessageCodec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Binary Message Codec using Protobuf
|
||||
*
|
||||
* This provides efficient binary serialization for IPC messages,
|
||||
* avoiding JSON stringify/parse overhead.
|
||||
*/
|
||||
|
||||
import * as protobuf from 'protobufjs'
|
||||
|
||||
// Define message schema
|
||||
const messageSchema = {
|
||||
nested: {
|
||||
mqtt: {
|
||||
nested: {
|
||||
Envelope: {
|
||||
fields: {
|
||||
topic: { type: 'string', id: 1 },
|
||||
payload: { type: 'bytes', id: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create root from JSON schema
|
||||
const root = protobuf.Root.fromJSON(messageSchema)
|
||||
const Envelope = root.lookupType('mqtt.Envelope')
|
||||
|
||||
export interface BinaryMessage {
|
||||
topic: string
|
||||
payload: Uint8Array
|
||||
}
|
||||
|
||||
export class MessageCodec {
|
||||
/**
|
||||
* Encode a message to binary format
|
||||
*/
|
||||
public static encode(topic: string, data: any): Uint8Array {
|
||||
// Serialize the payload to JSON, then to bytes
|
||||
const jsonString = JSON.stringify(data)
|
||||
const payloadBytes = new TextEncoder().encode(jsonString)
|
||||
|
||||
// Create protobuf envelope
|
||||
const message = Envelope.create({
|
||||
topic,
|
||||
payload: payloadBytes,
|
||||
})
|
||||
|
||||
// Encode to binary
|
||||
return Envelope.encode(message).finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a binary message
|
||||
*/
|
||||
public static decode(binary: Uint8Array): BinaryMessage {
|
||||
const message = Envelope.decode(binary) as any
|
||||
return {
|
||||
topic: message.topic,
|
||||
payload: message.payload,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and parse payload as JSON
|
||||
*/
|
||||
public static decodeWithPayload<T>(binary: Uint8Array): { topic: string; data: T } {
|
||||
const { topic, payload } = this.decode(binary)
|
||||
const jsonString = new TextDecoder().decode(payload)
|
||||
const data = JSON.parse(jsonString)
|
||||
return { topic, data }
|
||||
}
|
||||
}
|
||||
42
events/EventSystem/SocketIOClientEventBus.ts
Normal file
42
events/EventSystem/SocketIOClientEventBus.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Socket } from 'socket.io-client'
|
||||
import { CallbackStore } from './CallbackStore'
|
||||
import { EventBusInterface } from './EventBusInterface'
|
||||
import { Event } from '../Events'
|
||||
|
||||
export class SocketIOClientEventBus implements EventBusInterface {
|
||||
private socket: Socket
|
||||
private callbacks: Array<CallbackStore> = []
|
||||
|
||||
constructor(socket: Socket) {
|
||||
this.socket = socket
|
||||
}
|
||||
|
||||
public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||
const wrappedCallback = (arg: any) => {
|
||||
callback(arg)
|
||||
}
|
||||
console.log('subscribing', event.topic)
|
||||
this.socket.on(event.topic, wrappedCallback)
|
||||
this.callbacks.push({
|
||||
callback,
|
||||
wrappedCallback,
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||
this.socket.removeAllListeners(event.topic)
|
||||
}
|
||||
|
||||
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||
const item = this.callbacks.find(store => store.callback === callback)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
this.socket.off(event.topic, item.wrappedCallback)
|
||||
this.callbacks = this.callbacks.filter(a => a !== item)
|
||||
}
|
||||
|
||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||
this.socket.emit(event.topic, msg)
|
||||
}
|
||||
}
|
||||
274
events/EventSystem/SocketIOServerEventBus.ts
Normal file
274
events/EventSystem/SocketIOServerEventBus.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { Server as SocketIOServer, Socket } from 'socket.io'
|
||||
import { Event } from '../Events'
|
||||
import { EventBusInterface } from './EventBusInterface'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('mqtt-explorer:socketio')
|
||||
const debugConnect = Debug('mqtt-explorer:socketio:connect')
|
||||
const debugDisconnect = Debug('mqtt-explorer:socketio:disconnect')
|
||||
const debugSubscriptions = Debug('mqtt-explorer:socketio:subscriptions')
|
||||
const debugConnections = Debug('mqtt-explorer:socketio:connections')
|
||||
const debugEmit = Debug('mqtt-explorer:socketio:emit')
|
||||
|
||||
interface SocketSubscription {
|
||||
topic: string
|
||||
handler: (arg: any) => void
|
||||
}
|
||||
|
||||
export class SocketIOServerEventBus implements EventBusInterface {
|
||||
private io: SocketIOServer
|
||||
private clients: Map<string, Socket> = new Map() // socketId -> Socket
|
||||
|
||||
// Global handlers that apply to ALL sockets (like RPC endpoints)
|
||||
private globalHandlers: Map<string, (socket: Socket, arg: any) => void> = new Map()
|
||||
|
||||
// Per-socket subscriptions for cleanup
|
||||
private socketSubscriptions: Map<string, SocketSubscription[]> = new Map()
|
||||
|
||||
// Track which socket is currently processing a request
|
||||
private currentSocket: Socket | undefined
|
||||
|
||||
// Map connectionId -> socketId to route messages to correct client
|
||||
private connectionOwners: Map<string, string> = new Map()
|
||||
|
||||
// Track which connections to close when a socket disconnects
|
||||
private socketConnections: Map<string, Set<string>> = new Map()
|
||||
|
||||
constructor(io: SocketIOServer) {
|
||||
this.io = io
|
||||
|
||||
// Register connection handler once
|
||||
this.io.on('connection', socket => {
|
||||
debugConnect('Client connected: %s', socket.id)
|
||||
this.clients.set(socket.id, socket)
|
||||
this.socketSubscriptions.set(socket.id, [])
|
||||
this.socketConnections.set(socket.id, new Set())
|
||||
|
||||
// Register all global handlers on this socket
|
||||
this.globalHandlers.forEach((handler, topic) => {
|
||||
this.registerHandlerOnSocket(socket, topic, handler)
|
||||
})
|
||||
|
||||
// Log connection metrics
|
||||
this.logConnectionMetrics('connect', socket.id)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
debugDisconnect('Client disconnected: %s', socket.id)
|
||||
this.cleanupSocket(socket)
|
||||
this.clients.delete(socket.id)
|
||||
this.logConnectionMetrics('disconnect', socket.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private logConnectionMetrics(event: 'connect' | 'disconnect', socketId: string) {
|
||||
const totalClients = this.clients.size
|
||||
const totalSubscriptions = Array.from(this.socketSubscriptions.values()).reduce((sum, subs) => sum + subs.length, 0)
|
||||
const totalConnections = this.connectionOwners.size
|
||||
const socketSubs = this.socketSubscriptions.get(socketId)?.length || 0
|
||||
const socketConns = this.socketConnections.get(socketId)?.size || 0
|
||||
|
||||
debug(
|
||||
'[%s] clients=%d subscriptions=%d mqttConns=%d | socket[%s]: subs=%d conns=%d',
|
||||
event,
|
||||
totalClients,
|
||||
totalSubscriptions,
|
||||
|
||||
totalConnections,
|
||||
socketId.substring(0, 8),
|
||||
socketSubs,
|
||||
socketConns
|
||||
)
|
||||
|
||||
debugSubscriptions(
|
||||
'Total subscriptions: %d across %d sockets (avg: %d per socket)',
|
||||
totalSubscriptions,
|
||||
totalClients,
|
||||
totalClients > 0 ? Math.round(totalSubscriptions / totalClients) : 0
|
||||
)
|
||||
|
||||
debugConnections(
|
||||
'MQTT connections: %d total, %d owned by socket %s',
|
||||
totalConnections,
|
||||
socketConns,
|
||||
socketId.substring(0, 8)
|
||||
)
|
||||
}
|
||||
|
||||
private registerHandlerOnSocket(socket: Socket, topic: string, handler: (socket: Socket, arg: any) => void) {
|
||||
const wrappedHandler = (arg: any) => {
|
||||
this.currentSocket = socket
|
||||
|
||||
// Track connection ownership when a connection is added
|
||||
if (topic === 'connection/add/mqtt' && arg?.id) {
|
||||
this.connectionOwners.set(arg.id, socket.id)
|
||||
const socketConns = this.socketConnections.get(socket.id)
|
||||
if (socketConns) {
|
||||
socketConns.add(arg.id)
|
||||
}
|
||||
debugConnections(
|
||||
'Connection %s owned by socket %s (total: %d)',
|
||||
arg.id,
|
||||
socket.id.substring(0, 8),
|
||||
socketConns?.size || 0
|
||||
)
|
||||
}
|
||||
|
||||
// Remove connection ownership when a connection is removed
|
||||
if (topic === 'connection/remove' && typeof arg === 'string') {
|
||||
this.connectionOwners.delete(arg)
|
||||
const socketConns = this.socketConnections.get(socket.id)
|
||||
if (socketConns) {
|
||||
socketConns.delete(arg)
|
||||
}
|
||||
debugConnections(
|
||||
'Connection %s removed (socket %s remaining: %d)',
|
||||
arg,
|
||||
socket.id.substring(0, 8),
|
||||
socketConns?.size || 0
|
||||
)
|
||||
}
|
||||
|
||||
handler(socket, arg)
|
||||
}
|
||||
|
||||
socket.on(topic, wrappedHandler)
|
||||
|
||||
// Track subscription for cleanup
|
||||
const subscriptions = this.socketSubscriptions.get(socket.id)
|
||||
if (subscriptions) {
|
||||
subscriptions.push({ topic, handler: wrappedHandler })
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupSocket(socket: Socket) {
|
||||
debugDisconnect('Cleaning up socket %s', socket.id)
|
||||
|
||||
// Remove all event listeners for this socket
|
||||
const subscriptions = this.socketSubscriptions.get(socket.id)
|
||||
if (subscriptions) {
|
||||
subscriptions.forEach(({ topic, handler }) => {
|
||||
socket.off(topic, handler)
|
||||
})
|
||||
this.socketSubscriptions.delete(socket.id)
|
||||
debugSubscriptions('Removed %d subscriptions for socket %s', subscriptions.length, socket.id.substring(0, 8))
|
||||
}
|
||||
|
||||
// Close all MQTT connections owned by this socket
|
||||
const ownedConnections = this.socketConnections.get(socket.id)
|
||||
if (ownedConnections && ownedConnections.size > 0) {
|
||||
debugConnections(
|
||||
'Socket %s owned %d connections, requesting cleanup',
|
||||
socket.id.substring(0, 8),
|
||||
ownedConnections.size
|
||||
)
|
||||
|
||||
// Emit connection/remove for each owned connection
|
||||
// This will be handled by ConnectionManager to actually close the MQTT connection
|
||||
ownedConnections.forEach(connectionId => {
|
||||
debugConnections('Auto-closing connection %s (owner disconnected)', connectionId)
|
||||
// Simulate a remove request from this socket
|
||||
const removeHandler = this.globalHandlers.get('connection/remove')
|
||||
if (removeHandler) {
|
||||
this.currentSocket = socket
|
||||
removeHandler(socket, connectionId)
|
||||
}
|
||||
this.connectionOwners.delete(connectionId)
|
||||
})
|
||||
|
||||
this.socketConnections.delete(socket.id)
|
||||
}
|
||||
|
||||
// Remove from clients set
|
||||
this.clients.delete(socket.id)
|
||||
|
||||
// Clear current socket if it was this one
|
||||
if (this.currentSocket === socket) {
|
||||
this.currentSocket = undefined
|
||||
}
|
||||
|
||||
debugDisconnect('Cleanup complete for socket %s', socket.id.substring(0, 8))
|
||||
}
|
||||
|
||||
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||
const handler = (socket: Socket, arg: any) => {
|
||||
this.currentSocket = socket
|
||||
callback(arg)
|
||||
}
|
||||
|
||||
// Store as global handler
|
||||
this.globalHandlers.set(subscribeEvent.topic, handler)
|
||||
|
||||
// Register on all currently connected clients
|
||||
this.clients.forEach(client => {
|
||||
this.registerHandlerOnSocket(client, subscribeEvent.topic, handler)
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||
// Remove from global handlers
|
||||
this.globalHandlers.delete(event.topic)
|
||||
|
||||
// Remove from all sockets
|
||||
this.clients.forEach(client => {
|
||||
const subscriptions = this.socketSubscriptions.get(client.id)
|
||||
if (subscriptions) {
|
||||
const toRemove = subscriptions.filter(s => s.topic === event.topic)
|
||||
toRemove.forEach(({ handler }) => {
|
||||
client.off(event.topic, handler)
|
||||
})
|
||||
|
||||
// Update subscriptions list
|
||||
this.socketSubscriptions.set(
|
||||
client.id,
|
||||
subscriptions.filter(s => s.topic !== event.topic)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||
throw new Error('Not implemented - use unsubscribeAll instead')
|
||||
}
|
||||
|
||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||
const topic = event.topic
|
||||
|
||||
// Check if this is an RPC response (contains /response/ in topic)
|
||||
if (topic.includes('/response/')) {
|
||||
if (this.currentSocket && this.currentSocket.connected) {
|
||||
this.currentSocket.emit(topic, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a connection-specific event - optimized with early pattern match
|
||||
// Patterns: conn/${connectionId}, conn/state/${connectionId}, conn/publish/${connectionId}
|
||||
if (topic.startsWith('conn/')) {
|
||||
const parts = topic.split('/')
|
||||
let connectionId: string | undefined
|
||||
|
||||
if (parts.length === 2) {
|
||||
// conn/${connectionId}
|
||||
connectionId = parts[1]
|
||||
} else if (parts.length === 3 && (parts[1] === 'state' || parts[1] === 'publish')) {
|
||||
// conn/state/${connectionId} or conn/publish/${connectionId}
|
||||
connectionId = parts[2]
|
||||
}
|
||||
|
||||
if (connectionId) {
|
||||
const ownerSocketId = this.connectionOwners.get(connectionId)
|
||||
if (ownerSocketId) {
|
||||
const ownerSocket = this.clients.get(ownerSocketId)
|
||||
if (ownerSocket && ownerSocket.connected) {
|
||||
ownerSocket.emit(topic, msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All other events go to all clients
|
||||
this.io.emit(topic, msg)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user