Implementación completa de WebSockets para el SaaS de gestión de campañas
1 // server.ts — CultivaHub Socket.IO Server (escalado multi-instancia) 2 import { createServer } from 'http' 3 import { Server } from 'socket.io' 4 import { createAdapter } from '@socket.io/redis-adapter' 5 import { createClient } from 'redis' 6 import { setupAuth } from './middleware/socket-auth' 7 8 const pubClient = createClient({ url: 'redis://cultivahub-cache.euw1.cache.amazonaws.com' }) 9 const subClient = pubClient.duplicate() 10 await Promise.all([pubClient.connect(), subClient.connect()]) 11 12 const io = new Server(httpServer, { 13 cors: { origin: 'https://app.cultivahub.io', methods: ['GET', 'POST'] }, 14 pingTimeout: 60000, 15 pingInterval: 25000, 16 }) 17 18 io.adapter(createAdapter(pubClient, subClient)) // sync across EC2 instances 19 setupAuth(io) // JWT middleware 20 21 const onlineUsers = new Map<string, { socketId: string; userName: string; workspaceId: string }>() 22 23 io.on('connection', (socket) => { 24 const { userId, userName, workspaceId } = socket.data 25 26 // Auto-join workspace room 27 socket.join(`ws:${workspaceId}`) 28 29 // Presence — mark online 30 onlineUsers.set(userId, { socketId: socket.id, userName, workspaceId }) 31 io.to(`ws:${workspaceId}`).emit('presence-update', { userId, userName, status: 'online' }) 32 33 // Chat message in workspace 34 socket.on('chat:message', async ({ text, channelId }) => { 35 const msg = { userId, userName, text, channelId, ts: Date.now() } 36 await saveMessage(msg) // persist to PostgreSQL 37 io.to(`ws:${workspaceId}`).emit('chat:message', msg) 37 }) 38 39 // Typing indicator with debounce 40 socket.on('chat:typing', ({ channelId }) => { 41 socket.to(`ws:${workspaceId}`).emit('chat:typing', { userId, userName, channelId }) 42 }) 43 44 socket.on('disconnect', () => { 45 onlineUsers.delete(userId) 46 io.to(`ws:${workspaceId}`).emit('presence-update', { userId, status: 'offline' }) 47 }) 48 }) 49 50 // Push KPI metrics every 5s to workspace subscribers 51 setInterval(async () => { 52 const metrics = await fetchCampaignMetrics() 53 metrics.forEach((m) => io.to(`ws:${m.workspaceId}`).emit('kpi:update', m)) 54 }, 5000)