Ir al contenido principal
Versión: 4.x

Entrega del servidor

Hay dos formas comunes de sincronizar el estado del cliente al reconectarse:

  • el servidor envía todo el estado
  • o el cliente mantiene registro del último evento que procesó y el servidor envía las piezas faltantes

Ambas son soluciones totalmente válidas y elegir una dependerá de tu caso de uso. En este tutorial, iremos con la segunda.

Primero, persistamos los mensajes de nuestra aplicación de chat. Hoy hay muchas opciones geniales, usaremos SQLite aquí.

consejo

Si no estás familiarizado con SQLite, hay muchos tutoriales disponibles en línea, como este.

Instalemos los paquetes necesarios:

npm install sqlite sqlite3

Simplemente almacenaremos cada mensaje en una tabla SQL:

index.js
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');
const { Server } = require('socket.io');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');

async function main() {
// abrir el archivo de base de datos
const db = await open({
filename: 'chat.db',
driver: sqlite3.Database
});

// crear nuestra tabla 'messages' (puedes ignorar la columna 'client_offset' por ahora)
await db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_offset TEXT UNIQUE,
content TEXT
);
`);

const app = express();
const server = createServer(app);
const io = new Server(server, {
connectionStateRecovery: {}
});

app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});

io.on('connection', (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
// almacenar el mensaje en la base de datos
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO manejar el fallo
return;
}
// incluir el offset con el mensaje
io.emit('chat message', msg, result.lastID);
});
});

server.listen(3000, () => {
console.log('servidor corriendo en http://localhost:3000');
});
}

main();

El cliente luego mantendrá registro del offset:

index.html
<script>
const socket = io({
auth: {
serverOffset: 0
}
});

const form = document.getElementById('form');
const input = document.getElementById('input');
const messages = document.getElementById('messages');

form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});

socket.on('chat message', (msg, serverOffset) => {
const item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
socket.auth.serverOffset = serverOffset;
});
</script>

Y finalmente el servidor enviará los mensajes faltantes al (re)conectarse:

index.js
// [...]

io.on('connection', async (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO manejar el fallo
return;
}
io.emit('chat message', msg, result.lastID);
});

if (!socket.recovered) {
// si la recuperación del estado de conexión no fue exitosa
try {
await db.each('SELECT id, content FROM messages WHERE id > ?',
[socket.handshake.auth.serverOffset || 0],
(_err, row) => {
socket.emit('chat message', row.content, row.id);
}
)
} catch (e) {
// algo salió mal
}
}
});

// [...]

Veámoslo en acción:

Como puedes ver en el video anterior, funciona tanto después de una desconexión temporal como de un refresco completo de la página.

consejo

La diferencia con la característica de "Recuperación del estado de conexión" es que una recuperación exitosa podría no necesitar consultar tu base de datos principal (podría obtener los mensajes de un stream de Redis, por ejemplo).

OK, ahora hablemos de la entrega del cliente.