Real-time chat with Socket.IO, Redis and Mongo DB

Este proyecto es una prueba de concepto de un sistema de chat en tiempo real con soporte para múltiples salas, historial de mensajes y notificaciones. Está desarrollado con tecnologías pensadas para escalar en una arquitectura basada en microservicios.

Tecnologías utilizadas

  • Node.js + Express: Servidor HTTP y manejo de rutas.
  • Socket.IO: Comunicación en tiempo real con WebSockets.
  • MongoDB: Persistencia de mensajes y salas.
  • Redis: Sistema pub/sub para distribuir mensajes entre múltiples instancias del servidor.
  • HTML + JS: Cliente web simple.
  • PrimeFlex (opcional): Mejora visual de la interfaz.

Por qué Redis

Redis se usa para habilitar la publicación y suscripción de mensajes entre múltiples procesos o instancias del servidor. Esto es necesario cuando el servidor se ejecuta en un entorno distribuido o balanceado, ya que Socket.IO por sí solo no comparte información entre instancias.

Usar Redis asegura que todos los usuarios conectados reciban los mensajes aunque estén atendidos por diferentes procesos o servidores.

Estructura del Proyecto chat_0.1

Este proyecto tiene una arquitectura organizada para el desarrollo de una aplicación de chat utilizando Node.js con TypeScript y Redis. A continuación se describen los directorios y archivos principales:

Directorios

  • .git/
    Carpeta de configuración del repositorio Git. Contiene el historial de versiones y configuraciones de control de versiones.
  • node_modules/
    Directorio generado automáticamente por npm, contiene todas las dependencias del proyecto.
  • src/
    Carpeta principal del código fuente de la aplicación.
    • public/
      Directorio opcionalmente usado para archivos públicos (como HTML, CSS o JS accesibles directamente).
    • index.html
      Este archivo es la página principal que se muestra al abrir la aplicación en el navegador. Sirve como punto de entrada visual y normalmente contiene el diseño básico de la interfaz, además de los scripts necesarios para que la aplicación funcione.
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <title>Chat en Tiempo Real</title>
  <link rel="stylesheet" href="https://unpkg.com/primeflex@3.3.1/primeflex.min.css">
  <link rel="stylesheet" href="https://unpkg.com/primeicons/primeicons.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 2rem;
    }
    #chatBox {
      border: 1px solid #ccc;
      height: 250px;
      overflow-y: auto;
      padding: 1rem;
      background: #f4f4f4;
      border-radius: 6px;
    }
    #chatBox p {
      margin: 0.5rem 0;
    }
  </style>
</head>
<body>
  <h2 class="mb-3">💬 Chat en Tiempo Real</h2>

  <div class="flex flex-column gap-2 mb-3">
    <input id="username" class="p-inputtext p-component" placeholder="Tu nombre" />
    <input id="roomName" class="p-inputtext p-component" placeholder="Nombre de la sala" />
    <button class="p-button p-component" onclick="joinRoom()">Entrar</button>
  </div>

  <h3>Salas disponibles:</h3>
  <ul id="roomList" class="mb-3"></ul>

  <div id="chat" style="display:none;">
    <h3>Chat en Sala</h3>
    <div id="chatBox" class="mb-2"></div>
    <div class="flex gap-2">
      <input id="inputMsg" class="p-inputtext p-component flex-1" placeholder="Mensaje..." />
      <button class="p-button p-component" onclick="sendMessage()">Enviar</button>
    </div>
  </div>

  <audio id="notifSound" src="https://notificationsounds.com/storage/sounds/file-sounds-1154-pristine.mp3" preload="auto"></audio>

  <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
  <script>
    const socket = io();
    let currentRoom = '';

    // Pedir permiso para notificaciones
    if (Notification.permission !== 'granted') {
      Notification.requestPermission();
    }

    async function joinRoom() {
      const room = document.getElementById('roomName').value;
      const username = document.getElementById('username').value;
      if (!room || !username) return alert('Ingresa nombre y sala');

      currentRoom = room;
      document.getElementById('chat').style.display = 'block';
      clearMessages();

      socket.emit('joinRoom', room, username);

      socket.off('chatMessage');
      socket.on('chatMessage', (data) => {
        addMessage(`${data.user}: ${data.message}`);
        playNotificationSound();
        showNotification(data.user, data.message);
      });

      socket.off('notification');
      socket.on('notification', (msg) => {
        addMessage(`🔔 ${msg}`);
      });

      socket.off('chat-history');
      socket.on('chat-history', (messages) => {
        messages.forEach(msg => {
          addMessage(`${msg.user}: ${msg.message}`);
        });
      });

      addMessage(`🟢 Te uniste a la sala: ${room}`);
    }

    function sendMessage() {
      const message = document.getElementById('inputMsg').value;
      const user = document.getElementById('username').value;
      if (!message.trim()) return;
      socket.emit('chatMessage', { room: currentRoom, user, message });
      document.getElementById('inputMsg').value = '';
    }

    function addMessage(msg) {
      const box = document.getElementById('chatBox');
      const p = document.createElement('p');
      p.textContent = msg;
      box.appendChild(p);
      box.scrollTop = box.scrollHeight;
    }

    function clearMessages() {
      document.getElementById('chatBox').innerHTML = '';
    }

    async function fetchRooms() {
      try {
        const res = await fetch('/rooms');
        const rooms = await res.json();
        const list = document.getElementById('roomList');
        list.innerHTML = '';
        rooms.forEach(r => {
          const li = document.createElement('li');
          li.textContent = r.name;
          list.appendChild(li);
        });
      } catch (err) {
        console.error('Error al cargar salas:', err);
      }
    }

    function showNotification(user, message) {
      if (Notification.permission === 'granted') {
        new Notification(`💬 ${user}`, {
          body: message,
          icon: 'https://cdn-icons-png.flaticon.com/512/2331/2331942.png'
        });
      }
    }

    function playNotificationSound() {
      const sound = document.getElementById('notifSound');
      sound.currentTime = 0;
      sound.play().catch(() => {});
    }

    fetchRooms();
  </script>
</body>
</html>
  • db.ts
    Archivo de configuración y conexión a la base de datos (posiblemente MongoDB, PostgreSQL u otro).
import mongoose from 'mongoose';

await mongoose.connect('mongodb://localhost:27017/chat-app');

const messageSchema = new mongoose.Schema({
  room: String,
  user: String,
  message: String,
  timestamp: { type: Date, default: Date.now }
});

const roomSchema = new mongoose.Schema({
  name: { type: String, unique: true }
});

export const Message = mongoose.model('Message', messageSchema);
export const Room = mongoose.model('Room', roomSchema);
  • index.ts
    Punto de entrada principal de la aplicación. Es donde se inicializa y arranca el servidor o el core del sistema.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { pub, sub } from './redis.js';
import { Message, Room } from './db.js';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const server = http.createServer(app);
const io = new Server(server);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// Listar salas existentes
app.get('/rooms', async (_req, res) => {
  const rooms = await Room.find().select('name -_id');
  res.json(rooms);
});

io.on('connection', (socket) => {
  console.log('Cliente conectado');

  socket.on('joinRoom', async (room, username) => {
    socket.join(room);
    // Enviar historial al nuevo usuario
    const history = await Message.find({ room }).sort({ timestamp: 1 }).lean();
    socket.emit('chat-history', history);

    // Crear sala si no existe
    const exists = await Room.findOne({ name: room });
    if (!exists) {
      await new Room({ name: room }).save();
    }

    socket.to(room).emit('notification', `${username} se unió al chat`);
  });

  socket.on('chatMessage', async (data) => {
    const { room, user, message } = data;
    const chatMessage = { room, user, message };

    await pub.publish('chat', JSON.stringify(chatMessage));
    await new Message(chatMessage).save();
  });
});

await sub.subscribe('chat', (msg) => {
  const parsed = JSON.parse(msg);
  io.to(parsed.room).emit('chatMessage', parsed);
});

server.listen(3000,'0.0.0.0', () => {
  console.log('Servidor en http://localhost:3000');
});
  • redis.ts
    Configuración y lógica de conexión con Redis, utilizado probablemente para almacenamiento en caché o manejo de sesiones/chat en tiempo real.
import { createClient } from 'redis';

export const pub = createClient();
export const sub = createClient();

await pub.connect();
await sub.connect();
  • types.ts
    Archivo que define tipos y estructuras TypeScript personalizadas para mantener el tipado fuerte y organizado.
export interface ChatMessage {
  room: string;
  user: string;
  message: string;
}

export interface ChatRoom {
  name: string;
}

Archivos de Configuración

  • .gitignore
    Lista de archivos y carpetas que Git debe ignorar (por ejemplo: node_modules, logs, claves, etc.).
  • docker-compose.yml
    Archivo de configuración para Docker Compose. Define los servicios y contenedores necesarios para correr la aplicación en un entorno de contenedores (como Node, Redis, etc.).
  • package.json
    Archivo principal de configuración del proyecto Node.js. Contiene información del proyecto, scripts y dependencias.
  • package-lock.json
    Archivo generado automáticamente para bloquear versiones exactas de las dependencias, garantizando entornos consistentes.
  • README.md
    Archivo de documentación del proyecto. Suele contener instrucciones de instalación, uso y propósito del sistema.
  • tsconfig.json
    Configuración del compilador TypeScript. Define cómo se debe compilar el proyecto (paths, target, módulo, etc.).

Funcionalidades

  • Crear o unirse a salas desde el frontend
  • Listar salas disponibles
  • Enviar y recibir mensajes en tiempo real
  • Recibir historial de mensajes al ingresar a una sala
  • Persistencia de mensajes en MongoDB
  • Notificaciones dentro del chat
  • Exposición en red local usando 0.0.0.0

Código fuente:

GITHUB

¡Mensaje enviado!