Add Server-Sent Events (SSE) support for real-time synchronization
Despliegue Automático / desplegar (push) Successful in 1m38s
Despliegue Automático / desplegar (push) Successful in 1m38s
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const syncService = require('../services/syncService');
|
const syncService = require('../services/syncService');
|
||||||
|
const sseManager = require('../services/sseManager');
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
@@ -36,4 +37,35 @@ const sync = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { sync };
|
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 };
|
||||||
@@ -4,4 +4,6 @@ const syncController = require('../controllers/syncController');
|
|||||||
|
|
||||||
router.post('/sync', syncController.sync);
|
router.post('/sync', syncController.sync);
|
||||||
|
|
||||||
|
router.get('/stream', syncController.streamEvents);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -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();
|
||||||
@@ -2,7 +2,7 @@ const { Op } = require('sequelize');
|
|||||||
const sequelize = require('../config/database');
|
const sequelize = require('../config/database');
|
||||||
const Note = require('../models/Note');
|
const Note = require('../models/Note');
|
||||||
const Category = require('../models/Category');
|
const Category = require('../models/Category');
|
||||||
const { randomUUID } = require('crypto');
|
const sseManager = require('./sseManager');
|
||||||
|
|
||||||
const toIso = (value) => (value ? new Date(value).toISOString() : null);
|
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 {
|
return {
|
||||||
serverTimestamp: new Date().toISOString(),
|
serverTimestamp: new Date().toISOString(),
|
||||||
synced: true,
|
synced: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user