/** * Torch Handler * Handles multi-browser MCP control synchronization for the sync server. * Only one browser can have the "torch" and connect to MCP at a time. */ // Client metadata interface TorchClient { ws: any id: string name: string autoRequest: boolean userAgent: string hostname: string connectedAt: Date } // Connected torch clients (separate tracking from main clients set) const torchClients = new Map() let clientIdCounter = 1 // Torch state - who has control let torchHolderId: string | null = null function generateClientId(): string { return `client_${clientIdCounter++}_${Date.now().toString(36)}` } function broadcastTorchState(broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const clientList = Array.from(torchClients.values()).map(c => ({ id: c.id, name: c.name, autoRequest: c.autoRequest, userAgent: c.userAgent, hostname: c.hostname, connectedAt: c.connectedAt.toISOString(), hasTorch: c.id === torchHolderId })) const message = JSON.stringify({ type: 'torch-update', holderId: torchHolderId, clients: clientList }) broadcast(message) } /** * Handle torch client connection */ export function handleTorchConnect(ws: any, broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const id = generateClientId() torchClients.set(ws, { ws, id, name: 'Anonymous', autoRequest: false, userAgent: 'Unknown', hostname: 'Unknown', connectedAt: new Date() }) console.log(`[Torch] Client connected: ${id} (${torchClients.size} total)`) } /** * Handle torch messages */ export function handleTorchMessage(ws: any, data: any, broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const client = torchClients.get(ws) if (!client) return switch (data.type) { case 'register': { client.userAgent = data.userAgent || 'Unknown' client.hostname = data.hostname || 'Unknown' client.name = data.name || 'Anonymous' // Claim autoRequest exclusively (only one client at a time) if (data.autoRequest) { for (const [, c] of torchClients) { if (c.id !== client.id) c.autoRequest = false } client.autoRequest = true } // Auto-grant torch if requested and no one holds it if (client.autoRequest && torchHolderId === null) { torchHolderId = client.id console.log(`[Torch] Auto-granted torch to ${client.name} (${client.id})`) } const hasTorch = torchHolderId === client.id ws.send(JSON.stringify({ type: 'registered', id: client.id, hasTorch })) console.log(`[Torch] Registered: ${client.name} (${client.id}) (torch: ${hasTorch}, autoRequest: ${client.autoRequest})`) broadcastTorchState(broadcast) break } case 'set-auto-request': { const value = !!data.value if (value) { // Exclusive: clear autoRequest from all other clients for (const [, c] of torchClients) { if (c.id !== client.id) c.autoRequest = false } } client.autoRequest = value console.log(`[Torch] Auto-request ${value ? 'claimed by' : 'released by'}: ${client.name} (${client.id})`) broadcastTorchState(broadcast) break } case 'update-name': { const newName = (data.name || '').substring(0, 20) || 'Anonymous' client.name = newName console.log(`[Torch] Name updated: ${client.id} → ${newName}`) broadcastTorchState(broadcast) break } case 'request': { const previousHolder = torchHolderId torchHolderId = client.id ws.send(JSON.stringify({ type: 'granted' })) console.log(`[Torch] Transferred: ${previousHolder} → ${client.name} (${client.id})`) broadcastTorchState(broadcast) break } case 'release': { if (torchHolderId === client.id) { torchHolderId = null ws.send(JSON.stringify({ type: 'released' })) console.log(`[Torch] Released by: ${client.name} (${client.id})`) broadcastTorchState(broadcast) } break } case 'transfer': { // Transfer torch to a specific client (only if sender has torch or no one has it) const targetId = data.targetId if (!targetId) { ws.send(JSON.stringify({ type: 'error', message: 'targetId required' })) break } // Check if target exists let targetExists = false for (const [, c] of torchClients) { if (c.id === targetId) { targetExists = true break } } if (!targetExists) { ws.send(JSON.stringify({ type: 'error', message: 'Target client not found' })) break } // Only allow transfer if sender has torch or no one has it if (torchHolderId !== null && torchHolderId !== client.id) { ws.send(JSON.stringify({ type: 'error', message: 'Cannot transfer - you do not have the torch' })) break } const previousHolder = torchHolderId torchHolderId = targetId ws.send(JSON.stringify({ type: 'transferred', targetId })) console.log(`[Torch] Transferred by ${client.name} (${client.id}): ${previousHolder} → ${targetId}`) broadcastTorchState(broadcast) break } } } /** * Handle torch client disconnect */ export function handleTorchDisconnect(ws: any, broadcast: (message: string, filter?: (ws: any) => boolean) => void) { const client = torchClients.get(ws) if (client) { console.log(`[Torch] Client disconnected: ${client.name} (${client.id})`) // If this client had the torch, release it (no auto-assign) if (torchHolderId === client.id) { torchHolderId = null console.log(`[Torch] Torch released (holder disconnected)`) } torchClients.delete(ws) broadcastTorchState(broadcast) } } /** * Get torch status for health check */ export function getTorchStatus() { return { clients: torchClients.size, torchHolder: torchHolderId } } /** * Cleanup torch handler */ export function cleanupTorchHandler() { torchClients.clear() torchHolderId = null }