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 pornpm
, 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