diff --git a/src/controllers/syncController.js b/src/controllers/syncController.js index 5ed8080..a16973c 100644 --- a/src/controllers/syncController.js +++ b/src/controllers/syncController.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const syncService = require('../services/syncService'); +const sseManager = require('../services/sseManager'); const JWT_SECRET = process.env.JWT_SECRET; @@ -36,4 +37,35 @@ const sync = async (req, res) => { } }; -module.exports = { sync }; \ No newline at end of file +const streamEvents = async (req, res) => { + try { + // Validar token (usa tu lógica existente o un middleware) + const token = req.headers.authorization.slice(7).trim(); + const payload = jwt.verify(token, JWT_SECRET); + const userId = payload.id; + + // Configurar cabeceras obligatorias para SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); // Enviar cabeceras inmediatamente + + // Registrar este dispositivo en el manager + sseManager.addClient(userId, res); + + // Latido (Heartbeat) para mantener la conexión viva a través de firewalls + const interval = setInterval(() => res.write(':keepalive\n\n'), 30000); + + // Cuando el cliente (móvil/PC) cierre la app, limpiar la memoria + req.on('close', () => { + clearInterval(interval); + sseManager.removeClient(userId, res); + res.end(); + }); + + } catch (error) { + res.status(401).json({ error: 'No autorizado' }); + } +}; + +module.exports = { sync, streamEvents }; \ No newline at end of file diff --git a/src/routes/syncRoutes.js b/src/routes/syncRoutes.js index 2339226..555abe7 100644 --- a/src/routes/syncRoutes.js +++ b/src/routes/syncRoutes.js @@ -4,4 +4,6 @@ const syncController = require('../controllers/syncController'); router.post('/sync', syncController.sync); +router.get('/stream', syncController.streamEvents); + module.exports = router; \ No newline at end of file diff --git a/src/services/sseManager.js b/src/services/sseManager.js new file mode 100644 index 0000000..3419053 --- /dev/null +++ b/src/services/sseManager.js @@ -0,0 +1,39 @@ +class SseManager { + constructor() { + // Mapa para guardar los clientes conectados: { userId: [res1, res2, ...] } + this.clients = new Map(); + } + + addClient(userId, res) { + if (!this.clients.has(userId)) { + this.clients.set(userId, []); + } + this.clients.get(userId).push(res); + } + + removeClient(userId, res) { + if (this.clients.has(userId)) { + const userClients = this.clients.get(userId); + const index = userClients.indexOf(res); + if (index !== -1) userClients.splice(index, 1); + + // Limpiar el mapa si el usuario ya no tiene dispositivos conectados + if (userClients.length === 0) { + this.clients.delete(userId); + } + } + } + + // El "timbre": Avisar a todos los dispositivos de un usuario (excepto al que originó el cambio) + notifyUser(userId) { + if (this.clients.has(userId)) { + const userClients = this.clients.get(userId); + userClients.forEach(res => { + // Formato obligatorio de SSE: "data: {JSON}\n\n" + res.write(`data: ${JSON.stringify({ event: 'sync_needed' })}\n\n`); + }); + } + } +} + +module.exports = new SseManager(); \ No newline at end of file diff --git a/src/services/syncService.js b/src/services/syncService.js index a7e838d..50bab2a 100644 --- a/src/services/syncService.js +++ b/src/services/syncService.js @@ -2,7 +2,7 @@ const { Op } = require('sequelize'); const sequelize = require('../config/database'); const Note = require('../models/Note'); const Category = require('../models/Category'); -const { randomUUID } = require('crypto'); +const sseManager = require('./sseManager'); const toIso = (value) => (value ? new Date(value).toISOString() : null); @@ -217,6 +217,12 @@ class SyncService { }) ]); + if(incomingNotes && incomingNotes.length > 0 || + incomingCategories && incomingCategories.length > 0 + ) { + sseManager.notifyUser(userId); + } + return { serverTimestamp: new Date().toISOString(), synced: true,