Módulo 9: Testing y Debugging

⏱️ Tiempo estimado: 8-9 horas

📋 Prerequisites

  • Completar módulos 1-8
  • Conocer Node.js básico
  • Familiaridad con npm
  • Entender manejo de errores

🎯 Objetivos del Módulo

  • Implementar pruebas unitarias con Jest
  • Dominar técnicas de debugging
  • Usar herramientas de testing modernas
  • Aplicar TDD en desarrollo

📖 Testing con Jest

Configuración Básica

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}

// babel.config.js
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

Pruebas Unitarias Básicas

// calculadora.js
export function sumar(a, b) {
    return a + b;
}

export function restar(a, b) {
    return a - b;
}

// calculadora.test.js
import { sumar, restar } from './calculadora';

describe('Calculadora', () => {
    test('suma dos números correctamente', () => {
        expect(sumar(2, 3)).toBe(5);
        expect(sumar(-1, 1)).toBe(0);
        expect(sumar(0, 0)).toBe(0);
    });

    test('resta dos números correctamente', () => {
        expect(restar(5, 3)).toBe(2);
        expect(restar(1, 1)).toBe(0);
        expect(restar(0, 5)).toBe(-5);
    });
});

Testing Asíncrono

// api.js
export async function obtenerUsuario(id) {
    const response = await fetch(`/api/usuarios/${id}`);
    if (!response.ok) throw new Error('Usuario no encontrado');
    return response.json();
}

// api.test.js
import { obtenerUsuario } from './api';

describe('API', () => {
    test('obtiene usuario correctamente', async () => {
        const usuario = await obtenerUsuario(1);
        expect(usuario).toHaveProperty('id');
        expect(usuario).toHaveProperty('nombre');
    });

    test('maneja error cuando usuario no existe', async () => {
        await expect(obtenerUsuario(999))
            .rejects
            .toThrow('Usuario no encontrado');
    });
});

📖 Debugging Avanzado

Chrome DevTools

// Debugging en el navegador
function encontrarError() {
    debugger; // Punto de interrupción
    let suma = 0;
    for (let i = 0; i < 10; i++) {
        suma += i;
        console.log('Iteración:', i, 'Suma:', suma);
    }
    return suma;
}

// Console API avanzada
console.table([{a: 1, b: 2}, {a: 3, b: 4}]);
console.time('bucle');
for(let i = 0; i < 1000000; i++) {}
console.timeEnd('bucle');
console.trace('traza de la pila');

Performance Testing

// Medición de rendimiento
function medirTiempo(fn) {
    const inicio = performance.now();
    fn();
    const fin = performance.now();
    console.log(`Tiempo de ejecución: ${fin - inicio}ms`);
}

// Ejemplo de uso
medirTiempo(() => {
    // Código a medir
    const arr = new Array(1000000).fill(0);
    arr.map(x => x + 1);
});

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: TDD para Validador

// validador.test.js
describe('Validador', () => {
    test('valida email correctamente', () => {
        expect(validarEmail('test@example.com')).toBe(true);
        expect(validarEmail('invalid.email')).toBe(false);
    });

    test('valida contraseña correctamente', () => {
        expect(validarPassword('Abc123!')).toBe(true);
        expect(validarPassword('weak')).toBe(false);
    });
});

// validador.js
export function validarEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function validarPassword(password) {
    return /^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/.test(password);
}

Ejercicio 2: Mocking de API

// usuarios.test.js
import { obtenerUsuarios } from './usuarios';

jest.mock('./api');

describe('Usuarios Service', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });

    test('obtiene y procesa usuarios correctamente', async () => {
        const mockUsuarios = [
            { id: 1, nombre: 'Ana' },
            { id: 2, nombre: 'Luis' }
        ];

        api.obtenerUsuarios.mockResolvedValue(mockUsuarios);

        const resultado = await obtenerUsuarios();
        expect(resultado).toHaveLength(2);
        expect(api.obtenerUsuarios).toHaveBeenCalledTimes(1);
    });
});

💡 Mini Proyecto: Sistema de Testing E2E

// test-utils.js
export class TestHelper {
    static async esperarElemento(selector, timeout = 5000) {
        const inicio = Date.now();
        
        while (Date.now() - inicio < timeout) {
            const elemento = document.querySelector(selector);
            if (elemento) return elemento;
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        
        throw new Error(`Elemento ${selector} no encontrado después de ${timeout}ms`);
    }

    static async simularClick(selector) {
        const elemento = await this.esperarElemento(selector);
        elemento.click();
        return new Promise(resolve => setTimeout(resolve, 100));
    }

    static async simularInput(selector, valor) {
        const elemento = await this.esperarElemento(selector);
        elemento.value = valor;
        elemento.dispatchEvent(new Event('input'));
        elemento.dispatchEvent(new Event('change'));
        return new Promise(resolve => setTimeout(resolve, 100));
    }
}

// formulario.test.js
describe('Formulario de Registro', () => {
    beforeEach(() => {
        document.body.innerHTML = `
            <form id="registro">
                <input type="email" id="email">
                <input type="password" id="password">
                <button type="submit">Registrar</button>
            </form>
        `;
    });

    test('envía formulario correctamente', async () => {
        await TestHelper.simularInput('#email', 'test@example.com');
        await TestHelper.simularInput('#password', 'Abc123!');
        await TestHelper.simularClick('button[type="submit"]');

        // Verificar resultado
        const mensaje = await TestHelper.esperarElemento('.mensaje-exito');
        expect(mensaje.textContent).toContain('Registro exitoso');
    });
});

Módulo 10: Optimización y Rendimiento

⏱️ Tiempo estimado: 8-9 horas

📋 Prerequisites

  • Completar módulos 1-9
  • Conocimiento de herramientas de desarrollo del navegador
  • Entender el ciclo de vida de una aplicación web
  • Familiaridad con bundlers (webpack, vite, etc.)

🎯 Objetivos del Módulo

  • Optimizar el rendimiento de aplicaciones JavaScript
  • Implementar técnicas de lazy loading
  • Minimizar el impacto en memoria
  • Mejorar tiempos de carga y ejecución

📖 Optimización de Código

Memory Management

// Identificar memory leaks
let datosGrandes = null;

function cargarDatos() {
    datosGrandes = new Array(1000000);
    // Usar WeakMap para referencias débiles
    const cache = new WeakMap();
    
    // Limpiar cuando no se necesite
    function limpiar() {
        datosGrandes = null;
    }
}

// Evitar closures innecesarios
function crearFunciones() {
    const funciones = [];
    
    // ❌ Mal: Cada función mantiene su propio 'i'
    for(var i = 0; i < 10; i++) {
        funciones.push(function() { return i; });
    }
    
    // ✅ Bien: Usar let o const
    for(let i = 0; i < 10; i++) {
        funciones.push(function() { return i; });
    }
    
    return funciones;
}

Lazy Loading

// Lazy loading de módulos
async function cargarComponente() {
    const { Componente } = await import('./Componente.js');
    return new Componente();
}

// Lazy loading de imágenes
function observarImagenes() {
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                observer.unobserve(img);
            }
        });
    });

    document.querySelectorAll('img[data-src]')
        .forEach(img => observer.observe(img));
}

Event Debouncing y Throttling

// Debouncing
function debounce(fn, delay) {
    let timeoutId;
    
    return function (...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// Throttling
function throttle(fn, limit) {
    let inThrottle;
    
    return function (...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Ejemplo de uso
const buscarDebounced = debounce((texto) => {
    console.log('Buscando:', texto);
}, 300);

const scrollThrottled = throttle(() => {
    console.log('Scroll event');
}, 100);

window.addEventListener('scroll', scrollThrottled);

📖 Optimización del DOM

Virtual DOM Simple

class VirtualDOM {
    static createElement(type, props, ...children) {
        return { type, props, children };
    }

    static render(vnode, container) {
        if (typeof vnode === 'string' || typeof vnode === 'number') {
            container.appendChild(document.createTextNode(vnode));
            return;
        }

        const element = document.createElement(vnode.type);

        if (vnode.props) {
            Object.entries(vnode.props).forEach(([name, value]) => {
                if (name.startsWith('on')) {
                    element.addEventListener(
                        name.toLowerCase().slice(2),
                        value
                    );
                } else {
                    element.setAttribute(name, value);
                }
            });
        }

        vnode.children.forEach(child => 
            this.render(child, element)
        );

        container.appendChild(element);
    }
}

// Ejemplo de uso
const vdom = VirtualDOM.createElement('div', { class: 'container' },
    VirtualDOM.createElement('h1', null, 'Título'),
    VirtualDOM.createElement('p', null, 'Contenido')
);

VirtualDOM.render(vdom, document.body);

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Optimización de Lista

class ListaVirtual {
    constructor(container, items, itemHeight = 30) {
        this.container = container;
        this.items = items;
        this.itemHeight = itemHeight;
        this.visibleItems = Math.ceil(container.clientHeight / itemHeight);
        this.startIndex = 0;
        this.setup();
    }

    setup() {
        this.container.style.overflow = 'auto';
        this.container.style.position = 'relative';
        
        const totalHeight = this.items.length * this.itemHeight;
        this.container.innerHTML = `
            <div style="height: ${totalHeight}px;">
                <div class="items-container"></div>
            </div>
        `;
        
        this.itemsContainer = this.container
            .querySelector('.items-container');
        
        this.container.addEventListener('scroll', 
            this.handleScroll.bind(this));
        
        this.renderVisibleItems();
    }

    handleScroll() {
        const scrollTop = this.container.scrollTop;
        this.startIndex = Math.floor(scrollTop / this.itemHeight);
        this.renderVisibleItems();
    }

    renderVisibleItems() {
        const start = this.startIndex;
        const end = start + this.visibleItems + 2;
        
        this.itemsContainer.style.transform = 
            `translateY(${start * this.itemHeight}px)`;
        
        this.itemsContainer.innerHTML = this.items
            .slice(start, end)
            .map((item, index) => `
                <div style="height: ${this.itemHeight}px;">
                    ${item}
                </div>
            `)
            .join('');
    }
}

// Uso
const items = Array.from({ length: 10000 }, 
    (_, i) => `Item ${i + 1}`);

new ListaVirtual(
    document.querySelector('.lista-container'),
    items
);
class GaleriaOptimizada {
    constructor(container, imagenes) {
        this.container = container;
        this.imagenes = imagenes;
        this.imagenesCache = new Map();
        this.setup();
    }

    setup() {
        this.container.innerHTML = this.imagenes
            .map((img, index) => `
                <div class="imagen-container">
                    <img 
                        data-src="${img.url}"
                        alt="${img.alt}"
                        loading="lazy"
                        class="imagen"
                    >
                </div>
            `)
            .join('');

        this.observarImagenes();
        this.setupLightbox();
    }

    observarImagenes() {
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        this.cargarImagen(entry.target);
                        observer.unobserve(entry.target);
                    }
                });
            },
            {
                rootMargin: '50px'
            }
        );

        this.container
            .querySelectorAll('img[data-src]')
            .forEach(img => observer.observe(img));
    }

    async cargarImagen(img) {
        const src = img.dataset.src;
        
        if (!this.imagenesCache.has(src)) {
            this.imagenesCache.set(src, new Promise((resolve) => {
                const image = new Image();
                image.onload = () => resolve(src);
                image.src = src;
            }));
        }

        try {
            await this.imagenesCache.get(src);
            img.src = src;
            img.classList.add('cargada');
        } catch (error) {
            console.error('Error cargando imagen:', error);
            img.src = 'placeholder.jpg';
        }
    }

    setupLightbox() {
        const lightbox = document.createElement('div');
        lightbox.className = 'lightbox';
        document.body.appendChild(lightbox);

        this.container.addEventListener('click', (e) => {
            if (e.target.matches('.imagen')) {
                this.mostrarEnLightbox(e.target.src);
            }
        });

        lightbox.addEventListener('click', () => {
            lightbox.classList.remove('activo');
        });
    }

    mostrarEnLightbox(src) {
        const lightbox = document.querySelector('.lightbox');
        lightbox.innerHTML = `<img src="${src}">`;
        lightbox.classList.add('activo');
    }
}

// Estilos CSS necesarios
const styles = `
.imagen-container {
    margin: 10px;
    overflow: hidden;
}

.imagen {
    width: 100%;
    height: 200px;
    object-fit: cover;
    opacity: 0;
    transition: opacity 0.3s;
}

.imagen.cargada {
    opacity: 1;
}

.lightbox {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.9);
    display: none;
    justify-content: center;
    align-items: center;
}

.lightbox.activo {
    display: flex;
}

.lightbox img {
    max-width: 90%;
    max-height: 90%;
}
`;

// Uso
const imagenes = [
    { url: 'img1.jpg', alt: 'Imagen 1' },
    { url: 'img2.jpg', alt: 'Imagen 2' },
    // ... más imágenes
];

new GaleriaOptimizada(
    document.querySelector('.galeria'),
    imagenes
);

Módulo 11: Seguridad y Buenas Prácticas

⏱️ Tiempo estimado: 8-9 horas

📋 Prerequisites

  • Completar módulos 1-10
  • Conocimiento básico de HTTP/HTTPS
  • Entender conceptos de seguridad web
  • Familiaridad con tokens y autenticación

🎯 Objetivos del Módulo

  • Implementar prácticas de seguridad web
  • Prevenir vulnerabilidades comunes
  • Proteger datos sensibles
  • Aplicar principios de código seguro

📖 Seguridad Básica

Sanitización de Datos

class Sanitizador {
    static limpiarHTML(str) {
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }

    static limpiarURL(url) {
        try {
            const urlObj = new URL(url);
            return urlObj.toString();
        } catch {
            return '';
        }
    }

    static limpiarJSON(str) {
        try {
            const obj = JSON.parse(str);
            return JSON.stringify(obj);
        } catch {
            return '{}';
        }
    }
}

// Uso
const textoUsuario = '<script>alert("XSS")</script>';
console.log(Sanitizador.limpiarHTML(textoUsuario));

Prevención de XSS

class VistaSegura {
    static renderizar(contenedor, datos) {
        // Usar textContent en lugar de innerHTML
        contenedor.textContent = datos;
    }

    static crearElemento(tag, contenido, atributos = {}) {
        const elemento = document.createElement(tag);
        elemento.textContent = contenido;

        // Validar atributos permitidos
        const permitidos = ['class', 'id', 'href', 'src'];
        Object.entries(atributos).forEach(([key, value]) => {
            if (permitidos.includes(key)) {
                elemento.setAttribute(key, value);
            }
        });

        return elemento;
    }
}

// Ejemplo de uso
const contenido = VistaSegura.crearElemento('p', 
    'Contenido seguro', 
    { class: 'texto', onclick: 'alert(1)' } // onclick será ignorado
);

CSRF Protection

class CSRFProtection {
    static generarToken() {
        return crypto.randomUUID();
    }

    static configurarHeaders() {
        return {
            'X-CSRF-Token': this.obtenerToken(),
            'Content-Type': 'application/json'
        };
    }

    static verificarToken(token) {
        return token === this.obtenerToken();
    }

    static obtenerToken() {
        return document.querySelector('meta[name="csrf-token"]')
            ?.getAttribute('content');
    }
}

// Uso en peticiones
async function enviarDatosSeguro(url, datos) {
    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: CSRFProtection.configurarHeaders(),
            body: JSON.stringify(datos)
        });
        
        if (!response.ok) throw new Error('Error en la petición');
        return response.json();
    } catch (error) {
        console.error('Error:', error);
        throw error;
    }
}

📖 Buenas Prácticas de Seguridad

Validación de Entrada

class ValidadorEntrada {
    static email(email) {
        const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return regex.test(email);
    }

    static contraseña(password) {
        // Mínimo 8 caracteres, una mayúscula, un número y un carácter especial
        const regex = /^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
        return regex.test(password);
    }

    static numeroTelefono(telefono) {
        const regex = /^\+?[\d\s-]{10,}$/;
        return regex.test(telefono);
    }

    static url(url) {
        try {
            new URL(url);
            return true;
        } catch {
            return false;
        }
    }
}

// Uso
const formulario = {
    email: 'usuario@ejemplo.com',
    contraseña: 'Segura123!',
    telefono: '+1234567890'
};

Object.entries(formulario).forEach(([campo, valor]) => {
    if (ValidadorEntrada[campo]?.(valor)) {
        console.log(`${campo} válido`);
    } else {
        console.log(`${campo} inválido`);
    }
});

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Formulario Seguro

class FormularioSeguro {
    constructor(formularioId) {
        this.form = document.getElementById(formularioId);
        this.configurar();
    }

    configurar() {
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            if (this.validar()) {
                this.enviar();
            }
        });
    }

    validar() {
        const datos = new FormData(this.form);
        let valido = true;

        // Validar cada campo
        for (let [campo, valor] of datos.entries()) {
            if (!this.validarCampo(campo, valor)) {
                this.mostrarError(campo);
                valido = false;
            }
        }

        return valido;
    }

    validarCampo(campo, valor) {
        switch (campo) {
            case 'email':
                return ValidadorEntrada.email(valor);
            case 'password':
                return ValidadorEntrada.contraseña(valor);
            case 'telefono':
                return ValidadorEntrada.numeroTelefono(valor);
            default:
                return valor.length > 0;
        }
    }

    mostrarError(campo) {
        const elemento = this.form.querySelector(`[name="${campo}"]`);
        elemento.classList.add('error');
        
        const mensaje = document.createElement('span');
        mensaje.className = 'error-mensaje';
        mensaje.textContent = `${campo} inválido`;
        
        elemento.parentNode.appendChild(mensaje);
    }

    async enviar() {
        const datos = new FormData(this.form);
        try {
            const response = await enviarDatosSeguro(
                this.form.action,
                Object.fromEntries(datos)
            );
            this.mostrarExito(response);
        } catch (error) {
            this.mostrarError('form', 'Error al enviar el formulario');
        }
    }
}

// Uso
new FormularioSeguro('registro-form');

💡 Mini Proyecto: Sistema de Autenticación Seguro

class SistemaAutenticacion {
    constructor() {
        this.token = null;
        this.usuario = null;
    }

    async iniciarSesion(email, password) {
        try {
            const response = await fetch('/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ email, password })
            });

            if (!response.ok) throw new Error('Credenciales inválidas');

            const data = await response.json();
            this.establecerSesion(data);
            return true;
        } catch (error) {
            console.error('Error de autenticación:', error);
            throw error;
        }
    }

    establecerSesion(data) {
        this.token = data.token;
        this.usuario = data.usuario;
        
        // Guardar token de forma segura
        sessionStorage.setItem('auth_token', this.token);
        
        // Configurar interceptor para requests
        this.configurarInterceptor();
    }

    configurarInterceptor() {
        // Agregar token a todas las peticiones
        const originalFetch = window.fetch;
        window.fetch = async (...args) => {
            if (typeof args[1] === 'object') {
                if (!args[1].headers) {
                    args[1].headers = {};
                }
                args[1].headers['Authorization'] = `Bearer ${this.token}`;
            }
            try {
                const response = await originalFetch(...args);
                if (response.status === 401) {
                    // Token expirado o inválido
                    this.cerrarSesion();
                    throw new Error('Sesión expirada');
                }
                return response;
            } catch (error) {
                console.error('Error en petición:', error);
                throw error;
            }
        };
    }

    cerrarSesion() {
        this.token = null;
        this.usuario = null;
        sessionStorage.removeItem('auth_token');
        window.location.href = '/login';
    }

    verificarAutenticacion() {
        const token = sessionStorage.getItem('auth_token');
        if (!token) {
            throw new Error('No autenticado');
        }
        return true;
    }

    obtenerUsuario() {
        if (!this.verificarAutenticacion()) {
            return null;
        }
        return this.usuario;
    }
}

// Uso del sistema de autenticación
const auth = new SistemaAutenticacion();

document.getElementById('login-form')
    .addEventListener('submit', async (e) => {
        e.preventDefault();
        const email = document.getElementById('email').value;
        const password = document.getElementById('password').value;

        try {
            await auth.iniciarSesion(email, password);
            window.location.href = '/dashboard';
        } catch (error) {
            console.error('Error de login:', error);
        }
    });

Módulo 12: Frameworks y Herramientas Modernas

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-11
  • Conocimiento de npm y node
  • Familiaridad con bundlers
  • Entender módulos ES6

🎯 Objetivos del Módulo

  • Comprender los principales frameworks modernos
  • Configurar entornos de desarrollo profesionales
  • Implementar herramientas de build y bundling
  • Optimizar el flujo de desarrollo

📖 Herramientas de Desarrollo

Package Managers y Scripts

// package.json
{
  "name": "mi-proyecto",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint src --fix",
    "format": "prettier --write src",
    "test": "vitest"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "date-fns": "^2.30.0"
  },
  "devDependencies": {
    "vite": "^4.5.0",
    "eslint": "^8.53.0",
    "prettier": "^3.0.3",
    "vitest": "^0.34.6"
  }
}

ESLint Configuration

// .eslintrc.js
module.exports = {
    env: {
        browser: true,
        es2021: true,
        node: true
    },
    extends: [
        'eslint:recommended',
        'plugin:prettier/recommended'
    ],
    parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module'
    },
    rules: {
        'no-unused-vars': 'warn',
        'no-console': ['warn', { allow: ['warn', 'error'] }],
        'prefer-const': 'error'
    }
};

Vite Configuration

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    root: 'src',
    build: {
        outDir: '../dist',
        minify: 'terser',
        sourcemap: true
    },
    server: {
        port: 3000,
        open: true
    }
});

📖 Frameworks Modernos

React Básico

// App.jsx
import { useState, useEffect } from 'react';

function App() {
    const [contador, setContador] = useState(0);
    const [usuarios, setUsuarios] = useState([]);

    useEffect(() => {
        fetch('/api/usuarios')
            .then(res => res.json())
            .then(data => setUsuarios(data));
    }, []);

    return (
        <div>
            <h1>Contador: {contador}</h1>
            <button onClick={() => setContador(c => c + 1)}>
                Incrementar
            </button>
            
            <ul>
                {usuarios.map(usuario => (
                    <li key={usuario.id}>{usuario.nombre}</li>
                ))}
            </ul>
        </div>
    );
}

export default App;

Vue Básico

<!-- App.vue -->
<template>
    <div>
        <h1>Contador: </h1>
        <button @click="incrementar">
            Incrementar
        </button>
        
        <ul>
            <li v-for="usuario in usuarios" :key="usuario.id">
                
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            contador: 0,
            usuarios: []
        }
    },
    methods: {
        incrementar() {
            this.contador++
        }
    },
    async mounted() {
        const res = await fetch('/api/usuarios')
        this.usuarios = await res.json()
    }
}
</script>

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Configuración de Proyecto

# 1. Inicializar proyecto
mkdir mi-proyecto
cd mi-proyecto
npm init -y

# 2. Instalar dependencias
npm install vite @vitejs/plugin-react react react-dom
npm install -D eslint prettier

# 3. Crear estructura de archivos
mkdir src
touch src/index.html src/main.jsx src/App.jsx
touch .eslintrc.js .prettierrc

Ejercicio 2: Componente Reutilizable

// Button.jsx
function Button({ variant = 'primary', children, onClick }) {
    const styles = {
        primary: 'bg-blue-500 text-white',
        secondary: 'bg-gray-500 text-white',
        outline: 'border-2 border-blue-500'
    };

    return (
        <button 
            className={`px-4 py-2 rounded ${styles[variant]}`}
            onClick={onClick}
        >
            {children}
        </button>
    );
}

// Uso
function App() {
    return (
        <div>
            <Button onClick={() => alert('Primario')}>
                Botón Primario
            </Button>
            <Button variant="secondary">
                Botón Secundario
            </Button>
        </div>
    );
}

💡 Mini Proyecto: Panel de Admin con React

// AdminPanel.jsx
import { useState, useEffect } from 'react';
import { Table, Button, Modal, Form } from './components';

function AdminPanel() {
    const [usuarios, setUsuarios] = useState([]);
    const [modalAbierto, setModalAbierto] = useState(false);
    const [usuarioEditando, setUsuarioEditando] = useState(null);

    useEffect(() => {
        cargarUsuarios();
    }, []);

    async function cargarUsuarios() {
        try {
            const res = await fetch('/api/usuarios');
            const data = await res.json();
            setUsuarios(data);
        } catch (error) {
            console.error('Error cargando usuarios:', error);
        }
    }

    async function guardarUsuario(datos) {
        try {
            const url = usuarioEditando 
                ? `/api/usuarios/${usuarioEditando.id}`
                : '/api/usuarios';
                
            const method = usuarioEditando ? 'PUT' : 'POST';

            const res = await fetch(url, {
                method,
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(datos)
            });

            if (!res.ok) throw new Error('Error guardando usuario');

            await cargarUsuarios();
            setModalAbierto(false);
            setUsuarioEditando(null);
        } catch (error) {
            console.error('Error:', error);
        }
    }

    async function eliminarUsuario(id) {
        if (!confirm('¿Estás seguro?')) return;

        try {
            const res = await fetch(`/api/usuarios/${id}`, {
                method: 'DELETE'
            });

            if (!res.ok) throw new Error('Error eliminando usuario');

            await cargarUsuarios();
        } catch (error) {
            console.error('Error:', error);
        }
    }

    return (
        <div className="container mx-auto p-4">
            <div className="flex justify-between mb-4">
                <h1 className="text-2xl">Panel de Administración</h1>
                <Button 
                    onClick={() => setModalAbierto(true)}
                >
                    Nuevo Usuario
                </Button>
            </div>

            <Table
                data={usuarios}
                columns={[
                    { key: 'nombre', label: 'Nombre' },
                    { key: 'email', label: 'Email' },
                    { key: 'rol', label: 'Rol' }
                ]}
                actions={[
                    {
                        label: 'Editar',
                        onClick: (usuario) => {
                            setUsuarioEditando(usuario);
                            setModalAbierto(true);
                        }
                    },
                    {
                        label: 'Eliminar',
                        onClick: (usuario) => eliminarUsuario(usuario.id)
                    }
                ]}
            />

            <Modal
                isOpen={modalAbierto}
                onClose={() => {
                    setModalAbierto(false);
                    setUsuarioEditando(null);
                }}
            >
                <Form
                    initialValues={usuarioEditando || {}}
                    onSubmit={guardarUsuario}
                    fields={[
                        {
                            name: 'nombre',
                            label: 'Nombre',
                            type: 'text',
                            required: true
                        },
                        {
                            name: 'email',
                            label: 'Email',
                            type: 'email',
                            required: true
                        },
                        {
                            name: 'rol',
                            label: 'Rol',
                            type: 'select',
                            options: [
                                { value: 'admin', label: 'Administrador' },
                                { value: 'user', label: 'Usuario' }
                            ]
                        }
                    ]}
                />
            </Modal>
        </div>
    );
}

export default AdminPanel;

Módulo 13: APIs y Servicios Web Modernos

⏱️ Tiempo estimado: 8-9 horas

📋 Prerequisites

  • Completar módulos 1-12
  • Conocimiento de HTTP/HTTPS
  • Familiaridad con REST y JSON
  • Entender autenticación y autorización

🎯 Objetivos del Módulo

  • Crear y consumir APIs RESTful
  • Implementar GraphQL
  • Trabajar con WebSockets
  • Manejar autenticación JWT

📖 REST APIs Avanzado

Cliente API Moderno

class APIClient {
    constructor(baseURL, options = {}) {
        this.baseURL = baseURL;
        this.options = {
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            },
            timeout: options.timeout || 5000
        };
    }

    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);

        try {
            const response = await fetch(url, {
                ...this.options,
                ...options,
                headers: {
                    ...this.options.headers,
                    ...options.headers
                },
                signal: controller.signal
            });

            clearTimeout(timeoutId);

            if (!response.ok) {
                throw new APIError('Request failed', response.status, await response.json());
            }

            return response.json();
        } catch (error) {
            if (error.name === 'AbortError') {
                throw new Error('Request timeout');
            }
            throw error;
        }
    }

    get(endpoint, options = {}) {
        return this.request(endpoint, { ...options, method: 'GET' });
    }

    post(endpoint, data, options = {}) {
        return this.request(endpoint, {
            ...options,
            method: 'POST',
            body: JSON.stringify(data)
        });
    }

    put(endpoint, data, options = {}) {
        return this.request(endpoint, {
            ...options,
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }

    delete(endpoint, options = {}) {
        return this.request(endpoint, { ...options, method: 'DELETE' });
    }
}

class APIError extends Error {
    constructor(message, status, data) {
        super(message);
        this.status = status;
        this.data = data;
    }
}

// Uso
const api = new APIClient('https://api.ejemplo.com', {
    headers: {
        'Authorization': 'Bearer token123'
    }
});

try {
    const usuarios = await api.get('/usuarios');
    console.log(usuarios);
} catch (error) {
    console.error('Error:', error.message);
}

📖 GraphQL Básico

class GraphQLClient {
    constructor(endpoint) {
        this.endpoint = endpoint;
    }

    async query(query, variables = {}) {
        const response = await fetch(this.endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                query,
                variables
            })
        });

        const { data, errors } = await response.json();

        if (errors) {
            throw new Error(errors[0].message);
        }

        return data;
    }
}

// Ejemplo de uso
const client = new GraphQLClient('https://api.ejemplo.com/graphql');

const GET_USUARIO = `
    query GetUsuario($id: ID!) {
        usuario(id: $id) {
            id
            nombre
            email
            posts {
                id
                titulo
            }
        }
    }
`;

try {
    const { usuario } = await client.query(GET_USUARIO, { id: "123" });
    console.log(usuario);
} catch (error) {
    console.error('Error:', error.message);
}

📖 WebSockets

class WebSocketClient {
    constructor(url) {
        this.url = url;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.handlers = new Map();
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('Conectado');
            this.reconnectAttempts = 0;
        };

        this.ws.onmessage = (event) => {
            const { type, data } = JSON.parse(event.data);
            const handlers = this.handlers.get(type) || [];
            handlers.forEach(handler => handler(data));
        };

        this.ws.onclose = () => {
            console.log('Desconectado');
            this.reconnect();
        };

        this.ws.onerror = (error) => {
            console.error('Error:', error);
        };
    }

    reconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            console.log(`Reintentando conexión... (${this.reconnectAttempts})`);
            setTimeout(() => this.connect(), 1000 * this.reconnectAttempts);
        }
    }

    on(type, handler) {
        if (!this.handlers.has(type)) {
            this.handlers.set(type, []);
        }
        this.handlers.get(type).push(handler);
    }

    off(type, handler) {
        if (this.handlers.has(type)) {
            const handlers = this.handlers.get(type);
            const index = handlers.indexOf(handler);
            if (index !== -1) {
                handlers.splice(index, 1);
            }
        }
    }

    emit(type, data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({ type, data }));
        }
    }
}

// Uso
const ws = new WebSocketClient('wss://api.ejemplo.com/ws');

ws.on('mensaje', (data) => {
    console.log('Mensaje recibido:', data);
});

ws.on('notificacion', (data) => {
    console.log('Nueva notificación:', data);
});

// Enviar mensaje
ws.emit('mensaje', { texto: 'Hola mundo' });

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: API RESTful

// Implementar un CRUD completo para recursos
class RecursoAPI {
    constructor(baseURL, recurso) {
        this.api = new APIClient(baseURL);
        this.recurso = recurso;
    }

    obtenerTodos(params = {}) {
        return this.api.get(`/${this.recurso}`, { params });
    }

    obtenerPorId(id) {
        return this.api.get(`/${this.recurso}/${id}`);
    }

    crear(datos) {
        return this.api.post(`/${this.recurso}`, datos);
    }

    actualizar(id, datos) {
        return this.api.put(`/${this.recurso}/${id}`, datos);
    }

    eliminar(id) {
        return this.api.delete(`/${this.recurso}/${id}`);
    }
}

// Uso
const usuariosAPI = new RecursoAPI('https://api.ejemplo.com', 'usuarios');

// CRUD operations
async function ejemploCRUD() {
    try {
        // Crear
        const nuevoUsuario = await usuariosAPI.crear({
            nombre: 'Ana',
            email: 'ana@email.com'
        });

        // Leer
        const usuarios = await usuariosAPI.obtenerTodos();
        const usuario = await usuariosAPI.obtenerPorId(nuevoUsuario.id);

        // Actualizar
        await usuariosAPI.actualizar(usuario.id, {
            nombre: 'Ana García'
        });

        // Eliminar
        await usuariosAPI.eliminar(usuario.id);
    } catch (error) {
        console.error('Error:', error);
    }
}

💡 Mini Proyecto: Chat en Tiempo Real

class ChatApp {
    constructor(wsURL, apiURL) {
        this.ws = new WebSocketClient(wsURL);
        this.api = new APIClient(apiURL);
        this.usuarios = new Map();
        this.mensajes = [];
        this.setup();
    }

    async setup() {
        await this.cargarUsuarios();
        this.configurarWebSocket();
        this.renderizarUI();
    }

    async cargarUsuarios() {
        try {
            const usuarios = await this.api.get('/usuarios');
            usuarios.forEach(usuario => {
                this.usuarios.set(usuario.id, usuario);
            });
        } catch (error) {
            console.error('Error cargando usuarios:', error);
        }
    }

    configurarWebSocket() {
        this.ws.on('mensaje', (mensaje) => {
            this.mensajes.push(mensaje);
            this.renderizarMensaje(mensaje);
        });

        this.ws.on('usuario_conectado', (usuario) => {
            this.usuarios.set(usuario.id, usuario);
            this.mostrarNotificacion(`${usuario.nombre} se ha conectado`);
        });

        this.ws.on('usuario_desconectado', (usuarioId) => {
            const usuario = this.usuarios.get(usuarioId);
            if (usuario) {
                this.usuarios.delete(usuarioId);
                this.mostrarNotificacion(`${usuario.nombre} se ha desconectado`);
            }
        });
    }

    renderizarUI() {
        const container = document.createElement('div');
        container.className = 'chat-container';

        container.innerHTML = `
            <div class="chat-messages"></div>
            <div class="chat-input">
                <input type="text" placeholder="Escribe un mensaje...">
                <button>Enviar</button>
            </div>
        `;

        const input = container.querySelector('input');
        const button = container.querySelector('button');

        button.addEventListener('click', () => {
            const mensaje = input.value.trim();
            if (mensaje) {
                this.enviarMensaje(mensaje);
                input.value = '';
            }
        });

        input.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                button.click();
            }
        });

        document.body.appendChild(container);
    }

    renderizarMensaje(mensaje) {
        const container = document.querySelector('.chat-messages');
        const usuario = this.usuarios.get(mensaje.usuarioId);
        
        const elementoMensaje = document.createElement('div');
        elementoMensaje.className = 'mensaje';
        elementoMensaje.innerHTML = `
            <strong>${usuario?.nombre || 'Anónimo'}:</strong>
            <span>${mensaje.texto}</span>
            <small>${new Date(mensaje.timestamp).toLocaleTimeString()}</small>
        `;

        container.appendChild(elementoMensaje);
        container.scrollTop = container.scrollHeight;
    }

    mostrarNotificacion(mensaje) {
        const container = document.querySelector('.chat-messages');
        const notificacion = document.createElement('div');
        notificacion.className = 'notificacion';
        notificacion.textContent = mensaje;
        container.appendChild(notificacion);
        container.scrollTop = container.scrollHeight;
    }

    enviarMensaje(texto) {
        this.ws.emit('mensaje', {
            texto,
            timestamp: Date.now()
        });
    }
}

// Estilos CSS
const styles = `
.chat-container {
    width: 400px;
    height: 600px;
    border: 1px solid #ccc;
    border-radius: 8px;
    display: flex;
    flex-direction: column;
}

.chat-messages {
    flex: 1;
    overflow-y: auto;
    padding: 1rem;
}

.mensaje {
    margin-bottom: 0.5rem;
    padding: 0.5rem;
    background: #f5f5f5;
    border-radius: 4px;
}

.notificacion {
    text-align: center;
    color: #666;
    margin: 0.5rem 0;
    font-style: italic;
}

.chat-input {
    display: flex;
    padding: 1rem;
    border-top: 1px solid #eee;
}

.chat-input input {
    flex: 1;
    padding: 0.5rem;
    margin-right: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.chat-input button {
    padding: 0.5rem 1rem;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.chat-input button:hover {
    background: #0056b3;
}
`;

// Uso
const chat = new ChatApp(
    'wss://api.ejemplo.com/chat',
    'https://api.ejemplo.com'
);

Módulo 14: Arquitectura y Patrones Avanzados

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-13
  • Conocimiento sólido de patrones de diseño
  • Experiencia con arquitecturas de software
  • Familiaridad con sistemas distribuidos

🎯 Objetivos del Módulo

  • Implementar arquitecturas escalables
  • Aplicar patrones arquitectónicos modernos
  • Diseñar sistemas modulares y mantenibles
  • Gestionar estado complejo en aplicaciones

📖 Arquitecturas Modernas

Clean Architecture

// Estructura de carpetas recomendada
/*
src/
├── domain/           // Reglas de negocio y entidades
│   ├── entities/
│   ├── useCases/
│   └── repositories/
├── infrastructure/   // Implementaciones concretas
│   ├── database/
│   ├── api/
│   └── services/
├── interfaces/       // Adaptadores
│   ├── controllers/
│   ├── presenters/
│   └── gateways/
└── application/     // Casos de uso específicos
    ├── dto/
    └── services/
*/

// Ejemplo de Entity
class Usuario {
    constructor(id, nombre, email) {
        this.id = id;
        this.nombre = nombre;
        this.email = email;
    }

    validar() {
        if (!this.email.includes('@')) {
            throw new Error('Email inválido');
        }
    }
}

// Ejemplo de UseCase
class CrearUsuarioUseCase {
    constructor(usuarioRepository, emailService) {
        this.usuarioRepository = usuarioRepository;
        this.emailService = emailService;
    }

    async execute(userData) {
        const usuario = new Usuario(
            crypto.randomUUID(),
            userData.nombre,
            userData.email
        );

        usuario.validar();

        await this.usuarioRepository.save(usuario);
        await this.emailService.enviarBienvenida(usuario.email);

        return usuario;
    }
}

Arquitectura Hexagonal (Ports & Adapters)

// Puertos (Interfaces)
class UsuarioRepository {
    save(usuario) { throw new Error('Not implemented'); }
    findById(id) { throw new Error('Not implemented'); }
}

class EmailService {
    enviar(to, subject, body) { throw new Error('Not implemented'); }
}

// Adaptadores
class MongoUsuarioRepository extends UsuarioRepository {
    constructor(mongoClient) {
        super();
        this.mongoClient = mongoClient;
    }

    async save(usuario) {
        const collection = this.mongoClient.db().collection('usuarios');
        await collection.insertOne(usuario);
    }

    async findById(id) {
        const collection = this.mongoClient.db().collection('usuarios');
        return collection.findOne({ id });
    }
}

class SendgridEmailService extends EmailService {
    constructor(apiKey) {
        super();
        this.apiKey = apiKey;
    }

    async enviar(to, subject, body) {
        // Implementación con Sendgrid
    }
}

// Aplicación
class AplicacionUsuarios {
    constructor(usuarioRepository, emailService) {
        this.usuarioRepository = usuarioRepository;
        this.emailService = emailService;
    }

    async registrarUsuario(datos) {
        const usuario = new Usuario(datos);
        await this.usuarioRepository.save(usuario);
        await this.emailService.enviar(
            usuario.email,
            'Bienvenido',
            'Gracias por registrarte'
        );
    }
}

📖 Patrones Arquitectónicos

Event-Driven Architecture

class EventBus {
    constructor() {
        this.subscribers = new Map();
    }

    subscribe(event, callback) {
        if (!this.subscribers.has(event)) {
            this.subscribers.set(event, new Set());
        }
        this.subscribers.get(event).add(callback);
    }

    publish(event, data) {
        if (this.subscribers.has(event)) {
            this.subscribers.get(event).forEach(callback => {
                callback(data);
            });
        }
    }
}

// Uso en la aplicación
class OrdenService {
    constructor(eventBus) {
        this.eventBus = eventBus;
    }

    crearOrden(orden) {
        // Lógica para crear orden
        this.eventBus.publish('orden.creada', orden);
    }
}

class NotificacionService {
    constructor(eventBus) {
        eventBus.subscribe('orden.creada', this.manejarOrdenCreada.bind(this));
    }

    manejarOrdenCreada(orden) {
        // Enviar notificación
        console.log(`Orden ${orden.id} creada`);
    }
}

CQRS (Command Query Responsibility Segregation)

// Commands
class CrearUsuarioCommand {
    constructor(nombre, email) {
        this.nombre = nombre;
        this.email = email;
    }
}

// Queries
class ObtenerUsuarioQuery {
    constructor(id) {
        this.id = id;
    }
}

// Command Handler
class CrearUsuarioHandler {
    constructor(writeDb) {
        this.writeDb = writeDb;
    }

    async handle(command) {
        const usuario = {
            id: crypto.randomUUID(),
            nombre: command.nombre,
            email: command.email
        };

        await this.writeDb.usuarios.insert(usuario);
        return usuario.id;
    }
}

// Query Handler
class ObtenerUsuarioHandler {
    constructor(readDb) {
        this.readDb = readDb;
    }

    async handle(query) {
        return this.readDb.usuarios.findById(query.id);
    }
}

// Bus de comandos
class CommandBus {
    constructor() {
        this.handlers = new Map();
    }

    register(commandType, handler) {
        this.handlers.set(commandType.name, handler);
    }

    async dispatch(command) {
        const handler = this.handlers.get(command.constructor.name);
        if (!handler) {
            throw new Error(`No handler for ${command.constructor.name}`);
        }
        return handler.handle(command);
    }
}

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Implementar Repository Pattern

// Definir interface
class Repository {
    create(entity) { throw new Error('Not implemented'); }
    update(id, entity) { throw new Error('Not implemented'); }
    delete(id) { throw new Error('Not implemented'); }
    findById(id) { throw new Error('Not implemented'); }
    findAll() { throw new Error('Not implemented'); }
}

// Implementar para diferentes bases de datos
class MongoRepository extends Repository {
    constructor(collection) {
        super();
        this.collection = collection;
    }

    async create(entity) {
        return this.collection.insertOne(entity);
    }

    // Implementar otros métodos...
}

class PostgresRepository extends Repository {
    constructor(pool, table) {
        super();
        this.pool = pool;
        this.table = table;
    }

    async create(entity) {
        const query = `INSERT INTO ${this.table} SET ?`;
        return this.pool.query(query, entity);
    }

    // Implementar otros métodos...
}

💡 Mini Proyecto: Sistema de E-commerce

// Implementación completa en el repositorio del curso
class Ecommerce {
    constructor(
        productRepository,
        orderRepository,
        userRepository,
        eventBus
    ) {
        this.productRepository = productRepository;
        this.orderRepository = orderRepository;
        this.userRepository = userRepository;
        this.eventBus = eventBus;
    }

    async createOrder(userId, items) {
        const user = await this.userRepository.findById(userId);
        if (!user) throw new Error('Usuario no encontrado');

        const order = {
            id: crypto.randomUUID(),
            userId,
            items,
            status: 'pending',
            createdAt: new Date()
        };

        await this.orderRepository.create(order);
        this.eventBus.publish('order.created', order);

        return order;
    }

    // Implementar otros métodos...
}

Módulo 15: PWA y Características Web Modernas

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-14
  • Conocimiento de HTTP/HTTPS
  • Entender el ciclo de vida de aplicaciones web
  • Familiaridad con caché y almacenamiento web

🎯 Objetivos del Módulo

  • Implementar Progressive Web Apps (PWAs)
  • Utilizar Service Workers efectivamente
  • Manejar estados offline
  • Implementar notificaciones push

📖 Service Workers

Registro e Instalación

// sw.js
const CACHE_NAME = 'mi-app-v1';
const ASSETS = [
    '/',
    '/index.html',
    '/css/styles.css',
    '/js/app.js',
    '/images/logo.png'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(ASSETS))
    );
});

// Registro en la aplicación
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('SW registrado:', registration.scope);
            })
            .catch(error => {
                console.error('Error al registrar SW:', error);
            });
    });
}

Estrategias de Caché

// Cache First Strategy
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                if (response) {
                    return response;
                }
                return fetch(event.request)
                    .then(response => {
                        if (!response || response.status !== 200) {
                            return response;
                        }
                        const responseToCache = response.clone();
                        caches.open(CACHE_NAME)
                            .then(cache => {
                                cache.put(event.request, responseToCache);
                            });
                        return response;
                    });
            })
    );
});

// Network First Strategy
self.addEventListener('fetch', (event) => {
    event.respondWith(
        fetch(event.request)
            .catch(() => {
                return caches.match(event.request);
            })
    );
});

📖 Web Workers

// worker.js
self.addEventListener('message', (e) => {
    const { data } = e;
    
    switch (data.type) {
        case 'PROCESAR_DATOS':
            const resultado = procesarDatosIntensivos(data.payload);
            self.postMessage({ type: 'RESULTADO', payload: resultado });
            break;
    }
});

function procesarDatosIntensivos(datos) {
    // Simulación de procesamiento pesado
    return datos.map(item => item * 2);
}

// Uso en la aplicación
class WorkerManager {
    constructor() {
        this.worker = new Worker('worker.js');
        this.setupEventListeners();
    }

    setupEventListeners() {
        this.worker.onmessage = (e) => {
            const { type, payload } = e.data;
            switch (type) {
                case 'RESULTADO':
                    this.manejarResultado(payload);
                    break;
            }
        };

        this.worker.onerror = (error) => {
            console.error('Error en worker:', error);
        };
    }

    procesarDatos(datos) {
        this.worker.postMessage({
            type: 'PROCESAR_DATOS',
            payload: datos
        });
    }

    manejarResultado(resultado) {
        console.log('Resultado procesado:', resultado);
    }

    terminar() {
        this.worker.terminate();
    }
}

📖 Notificaciones Push

class NotificacionManager {
    constructor() {
        this.publicKey = 'TU_CLAVE_PUBLICA_VAPID';
    }

    async solicitarPermiso() {
        const permiso = await Notification.requestPermission();
        if (permiso !== 'granted') {
            throw new Error('Permiso de notificaciones denegado');
        }
        return this.suscribirse();
    }

    async suscribirse() {
        const registration = await navigator.serviceWorker.ready;
        const subscription = await registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: this.urlBase64ToUint8Array(this.publicKey)
        });

        return this.enviarSuscripcionAlServidor(subscription);
    }

    async enviarSuscripcionAlServidor(subscription) {
        const response = await fetch('/api/suscripciones', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(subscription)
        });

        if (!response.ok) {
            throw new Error('Error al guardar suscripción');
        }

        return subscription;
    }

    urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
            .replace(/\-/g, '+')
            .replace(/_/g, '/');

        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);

        for (let i = 0; i < rawData.length; ++i) {
            outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
    }
}

// Service Worker: Manejo de notificaciones push
self.addEventListener('push', (event) => {
    const options = {
        body: event.data.text(),
        icon: '/icon.png',
        badge: '/badge.png',
        vibrate: [100, 50, 100],
        data: {
            fechaCreacion: Date.now(),
            url: 'https://ejemplo.com'
        },
        actions: [
            {
                action: 'explorar',
                title: 'Ver más'
            },
            {
                action: 'cerrar',
                title: 'Cerrar'
            }
        ]
    };

    event.waitUntil(
        self.registration.showNotification('Mi App', options)
    );
});

self.addEventListener('notificationclick', (event) => {
    event.notification.close();

    if (event.action === 'explorar') {
        event.waitUntil(
            clients.openWindow(event.notification.data.url)
        );
    }
});

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Implementar Modo Offline

class OfflineManager {
    constructor() {
        this.db = null;
        this.initDB();
    }

    async initDB() {
        const request = indexedDB.open('OfflineDB', 1);

        request.onerror = () => {
            console.error('Error al abrir DB');
        };

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            const store = db.createObjectStore('pendientes', {
                keyPath: 'id',
                autoIncrement: true
            });
            store.createIndex('tipo', 'tipo');
        };

        request.onsuccess = (event) => {
            this.db = event.target.result;
        };
    }

    async guardarOperacionPendiente(operacion) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction(['pendientes'], 'readwrite');
            const store = transaction.objectStore('pendientes');
            const request = store.add(operacion);

            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }

    async sincronizar() {
        const pendientes = await this.obtenerPendientes();
        for (const operacion of pendientes) {
            try {
                await this.ejecutarOperacion(operacion);
                await this.eliminarOperacion(operacion.id);
            } catch (error) {
                console.error('Error al sincronizar:', error);
            }
        }
    }

    async ejecutarOperacion(operacion) {
        const response = await fetch('/api/sincronizar', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(operacion)
        });

        if (!response.ok) {
            throw new Error('Error al sincronizar operación');
        }
    }
}

💡 Mini Proyecto: PWA para Blog

// app.js
class BlogPWA {
    constructor() {
        this.offlineManager = new OfflineManager();
        this.notificacionManager = new NotificacionManager();
        this.setupUI();
        this.cargarPosts();
    }

    async setupUI() {
        const container = document.querySelector('#blog-container');
        
        // Botón de instalación
        window.addEventListener('beforeinstallprompt', (e) => {
            e.preventDefault();
            const installBtn = document.createElement('button');
            installBtn.textContent = 'Instalar App';
            installBtn.onclick = () => e.prompt();
            container.prepend(installBtn);
        });

        // Botón de notificaciones
        const notifyBtn = document.createElement('button');
        notifyBtn.textContent = 'Activar Notificaciones';
        notifyBtn.onclick = () => this.notificacionManager.solicitarPermiso();
        container.prepend(notifyBtn);
    }

    async cargarPosts() {
        try {
            const posts = await this.obtenerPosts();
            this.renderizarPosts(posts);
        } catch (error) {
            console.error('Error cargando posts:', error);
            this.mostrarError('Error al cargar posts');
        }
    }

    async obtenerPosts() {
        try {
            const response = await fetch('/api/posts');
            return response.json();
        } catch (error) {
            // Si estamos offline, usar caché
            const cache = await caches.open(CACHE_NAME);
            const response = await cache.match('/api/posts');
            if (response) return response.json();
            throw error;
        }
    }

    renderizarPosts(posts) {
        const container = document.querySelector('#posts');
        container.innerHTML = posts
            .map(post => `
                <article class="post">
                    <h2>${post.titulo}</h2>
                    <p>${post.extracto}</p>
                    <button onclick="blog.guardarParaLeerDespues(${post.id})">
                        Guardar
                    </button>
                </article>
            `)
            .join('');
    }

    async guardarParaLeerDespues(postId) {
        await this.offlineManager.guardarOperacionPendiente({
            tipo: 'GUARDAR_POST',
            postId
        });
        this.mostrarMensaje('Post guardado para leer después');
    }

    mostrarMensaje(mensaje) {
        const div = document.createElement('div');
        div.className = 'mensaje';
        div.textContent = mensaje;
        document.body.appendChild(div);
        setTimeout(() => div.remove(), 3000);
    }

    mostrarError(mensaje) {
        const div = document.createElement('div');
        div.className = 'error';
        div.textContent = mensaje;
        document.body.appendChild(div);
    }
}

// Estilos CSS
const styles = `
.post {
    padding: 1rem;
    margin: 1rem 0;
    border: 1px solid #ddd;
    border-radius: 8px;
}

.mensaje {
    position: fixed;
    bottom: 20px;
    right: 20px;
    padding: 1rem;
    background: #4CAF50;
    color: white;
    border-radius: 4px;
    animation: slideIn 0.3s ease-out;
}

.error {
    background: #f44336;
}

@keyframes slideIn {
    from {
        transform: translateY(100px);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}
`;

// Inicializar
const blog = new BlogPWA();

Módulo 16: CI/CD y DevOps para JavaScript

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-15
  • Conocimiento básico de Git
  • Familiaridad con línea de comandos
  • Entender conceptos de integración continua

🎯 Objetivos del Módulo

  • Implementar pipelines de CI/CD
  • Configurar entornos automatizados
  • Gestionar despliegues seguros
  • Monitorear aplicaciones en producción

📖 Configuración de CI/CD

GitHub Actions

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test
      
      - name: Run linting
        run: npm run lint
      
      - name: Build application
        run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to production
        run: |
          echo "Deploying to production..."
          # Aquí irían los comandos de despliegue

Docker Configuration

FROM node:18-alpine as builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Docker Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:80"
    environment:
      - NODE_ENV=production
      - API_URL=https://api.example.com

📖 Automatización de Pruebas

Jest Configuration

// jest.config.js
module.exports = {
    testEnvironment: 'jsdom',
    setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
    moduleNameMapper: {
        '\\.(css|less|scss)$': 'identity-obj-proxy',
        '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
    },
    collectCoverageFrom: [
        'src/**/*.{js,jsx}',
        '!src/**/*.test.{js,jsx}',
        '!src/index.js'
    ],
    coverageThreshold: {
        global: {
            branches: 80,
            functions: 80,
            lines: 80,
            statements: 80
        }
    }
};

E2E Testing con Cypress

// cypress/e2e/login.cy.js
describe('Login Flow', () => {
    beforeEach(() => {
        cy.visit('/login');
    });

    it('should login successfully', () => {
        cy.get('[data-test="email-input"]')
            .type('user@example.com');
        
        cy.get('[data-test="password-input"]')
            .type('password123');
        
        cy.get('[data-test="login-button"]')
            .click();
        
        cy.url().should('include', '/dashboard');
        cy.get('[data-test="welcome-message"]')
            .should('contain', 'Welcome');
    });

    it('should show error on invalid credentials', () => {
        cy.get('[data-test="email-input"]')
            .type('invalid@example.com');
        
        cy.get('[data-test="password-input"]')
            .type('wrongpassword');
        
        cy.get('[data-test="login-button"]')
            .click();
        
        cy.get('[data-test="error-message"]')
            .should('be.visible')
            .and('contain', 'Invalid credentials');
    });
});

📖 Monitoreo y Logging

Winston Logger Setup

// logger.js
import winston from 'winston';

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ 
            filename: 'error.log', 
            level: 'error' 
        }),
        new winston.transports.File({ 
            filename: 'combined.log' 
        })
    ]
});

if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.simple()
    }));
}

Performance Monitoring

// monitoring.js
class PerformanceMonitor {
    constructor() {
        this.metrics = new Map();
    }

    startTimer(label) {
        this.metrics.set(label, performance.now());
    }

    endTimer(label) {
        const startTime = this.metrics.get(label);
        if (!startTime) return;

        const duration = performance.now() - startTime;
        logger.info(`Performance: ${label}`, { 
            duration,
            timestamp: new Date(),
            label
        });

        this.metrics.delete(label);
        return duration;
    }

    trackMemory() {
        const used = process.memoryUsage();
        logger.info('Memory Usage', {
            heapTotal: used.heapTotal / 1024 / 1024,
            heapUsed: used.heapUsed / 1024 / 1024,
            external: used.external / 1024 / 1024,
            timestamp: new Date()
        });
    }
}

export const monitor = new PerformanceMonitor();

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Pipeline Automatizado

// scripts/deploy.js
const { exec } = require('child_process');
const fs = require('fs').promises;

async function deploy() {
    try {
        // Verificar tests
        await executeCommand('npm test');
        
        // Build
        await executeCommand('npm run build');
        
        // Verificar archivos necesarios
        await validateBuild();
        
        // Desplegar
        await executeCommand('firebase deploy');
        
        console.log('Despliegue exitoso!');
    } catch (error) {
        console.error('Error en el despliegue:', error);
        process.exit(1);
    }
}

async function executeCommand(command) {
    return new Promise((resolve, reject) => {
        exec(command, (error, stdout, stderr) => {
            if (error) {
                console.error(`Error ejecutando ${command}:`, stderr);
                reject(error);
                return;
            }
            console.log(stdout);
            resolve(stdout);
        });
    });
}

async function validateBuild() {
    const files = await fs.readdir('./dist');
    const required = ['index.html', 'main.js', 'style.css'];
    
    for (const file of required) {
        if (!files.includes(file)) {
            throw new Error(`Archivo requerido ${file} no encontrado`);
        }
    }
}

deploy();

💡 Mini Proyecto: Sistema de Despliegue Automatizado

// deploymentSystem.js
class DeploymentSystem {
    constructor(config) {
        this.config = config;
        this.steps = [];
        this.hooks = new Map();
    }

    addStep(name, fn) {
        this.steps.push({ name, fn });
    }

    addHook(event, fn) {
        if (!this.hooks.has(event)) {
            this.hooks.set(event, []);
        }
        this.hooks.get(event).push(fn);
    }

    async executeHooks(event, data) {
        const hooks = this.hooks.get(event) || [];
        for (const hook of hooks) {
            await hook(data);
        }
    }

    async deploy() {
        try {
            await this.executeHooks('beforeDeploy', {
                timestamp: new Date(),
                config: this.config
            });

            for (const step of this.steps) {
                console.log(`Ejecutando paso: ${step.name}`);
                await step.fn(this.config);
                
                await this.executeHooks('afterStep', {
                    step: step.name,
                    timestamp: new Date()
                });
            }

            await this.executeHooks('afterDeploy', {
                success: true,
                timestamp: new Date()
            });

            console.log('Despliegue completado exitosamente');
        } catch (error) {
            console.error('Error en el despliegue:', error);
            
            await this.executeHooks('onError', {
                error,
                timestamp: new Date()
            });
            
            throw error;
        }
    }
}

// Uso del sistema
const deploymentSystem = new DeploymentSystem({
    environment: 'production',
    region: 'us-east-1',
    version: '1.0.0'
});

// Configurar pasos
deploymentSystem.addStep('validate', async (config) => {
    // Validar configuración
});

deploymentSystem.addStep('build', async (config) => {
    // Construir aplicación
});

deploymentSystem.addStep('test', async (config) => {
    // Ejecutar pruebas
});

deploymentSystem.addStep('deploy', async (config) => {
    // Desplegar a producción
});

// Configurar hooks
deploymentSystem.addHook('beforeDeploy', async (data) => {
    logger.info('Iniciando despliegue', data);
});

deploymentSystem.addHook('afterDeploy', async (data) => {
    logger.info('Despliegue completado', data);
    // Notificar al equipo
});

deploymentSystem.addHook('onError', async (data) => {
    logger.error('Error en el despliegue', data);
    // Notificar al equipo de errores
});

// Ejecutar despliegue
deploymentSystem.deploy()
    .then(() => console.log('Proceso completado'))
    .catch(error => console.error('Proceso fallido:', error));

Módulo 17: Rendimiento y Optimización Avanzada

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-16
  • Conocimiento de DevTools y profiling
  • Familiaridad con métricas de rendimiento
  • Entender el ciclo de vida del navegador

🎯 Objetivos del Módulo

  • Optimizar Core Web Vitals
  • Implementar técnicas de renderizado eficiente
  • Reducir tiempo de carga inicial
  • Mejorar la experiencia del usuario

📖 Core Web Vitals

Performance Metrics Monitor

class PerformanceMetrics {
    constructor() {
        this.metrics = {};
        this.observers = new Set();
    }

    observe() {
        // Largest Contentful Paint
        new PerformanceObserver((entries) => {
            const lcp = entries.getEntries().at(-1);
            this.updateMetric('LCP', lcp.startTime);
        }).observe({ entryTypes: ['largest-contentful-paint'] });

        // First Input Delay
        new PerformanceObserver((entries) => {
            const fid = entries.getEntries()[0];
            this.updateMetric('FID', fid.processingStart - fid.startTime);
        }).observe({ entryTypes: ['first-input'] });

        // Cumulative Layout Shift
        new PerformanceObserver((entries) => {
            let cls = 0;
            entries.getEntries().forEach(entry => {
                cls += entry.value;
            });
            this.updateMetric('CLS', cls);
        }).observe({ entryTypes: ['layout-shift'] });
    }

    updateMetric(name, value) {
        this.metrics[name] = value;
        this.notifyObservers();
    }

    addObserver(callback) {
        this.observers.add(callback);
    }

    notifyObservers() {
        this.observers.forEach(callback => callback(this.metrics));
    }
}

// Uso
const metrics = new PerformanceMetrics();
metrics.observe();

metrics.addObserver((metrics) => {
    console.log('Core Web Vitals:', metrics);
});

📚 Virtual List Rendering

class VirtualList {
    constructor(container, items, options = {}) {
        this.container = container;
        this.items = items;
        this.options = {
            itemHeight: options.itemHeight || 50,
            overscan: options.overscan || 5,
            ...options
        };
        
        this.visibleItems = Math.ceil(container.clientHeight / this.options.itemHeight);
        this.totalHeight = items.length * this.options.itemHeight;
        this.startIndex = 0;
        
        this.setupContainer();
        this.render();
        this.attachEvents();
    }

    setupContainer() {
        this.container.style.position = 'relative';
        this.container.style.overflow = 'auto';
        
        this.content = document.createElement('div');
        this.content.style.height = `${this.totalHeight}px`;
        this.content.style.position = 'relative';
        
        this.container.appendChild(this.content);
    }

    render() {
        const start = Math.max(0, this.startIndex - this.options.overscan);
        const end = Math.min(
            this.items.length,
            this.startIndex + this.visibleItems + this.options.overscan
        );

        const fragment = document.createDocumentFragment();
        
        for (let i = start; i < end; i++) {
            const item = this.renderItem(this.items[i], i);
            item.style.position = 'absolute';
            item.style.top = `${i * this.options.itemHeight}px`;
            item.style.width = '100%';
            fragment.appendChild(item);
        }

        this.content.innerHTML = '';
        this.content.appendChild(fragment);
    }

    renderItem(data, index) {
        const div = document.createElement('div');
        div.style.height = `${this.options.itemHeight}px`;
        div.textContent = data.toString();
        return div;
    }

    attachEvents() {
        this.container.addEventListener('scroll', () => {
            requestAnimationFrame(() => {
                const newIndex = Math.floor(
                    this.container.scrollTop / this.options.itemHeight
                );
                
                if (newIndex !== this.startIndex) {
                    this.startIndex = newIndex;
                    this.render();
                }
            });
        });
    }
}

🌐 Router

// router.js
const routes = {
    '/': () => import('./pages/Home'),
    '/about': () => import('./pages/About'),
    '/dashboard': () => import('./pages/Dashboard')
};

class Router {
    constructor(container) {
        this.container = container;
        this.currentComponent = null;
        
        window.addEventListener('popstate', () => this.handleRoute());
        this.handleRoute();
    }

    async handleRoute() {
        const path = window.location.pathname;
        const route = routes[path];

        if (route) {
            try {
                const module = await route();
                const Component = module.default;
                
                if (this.currentComponent) {
                    this.currentComponent.destroy?.();
                }
                
                this.currentComponent = new Component(this.container);
                this.currentComponent.render();
            } catch (error) {
                console.error('Error loading route:', error);
            }
        }
    }
}

📦 Bundle Analyzer

// utils.js
export function analyzeBundle() {
    const usedExports = new Set();
    const unusedExports = new Set();

    // Analizar imports en el código
    function scanImports(sourceCode) {
        const importRegex = /import\s+{([^}]+)}\s+from/g;
        let match;

        while ((match = importRegex.exec(sourceCode)) !== null) {
            const imports = match[1].split(',')
                .map(s => s.trim())
                .filter(Boolean);
            
            imports.forEach(name => usedExports.add(name));
        }
    }

    // Analizar exports
    function scanExports(sourceCode) {
        const exportRegex = /export\s+(?:const|let|function|class)\s+(\w+)/g;
        let match;

        while ((match = exportRegex.exec(sourceCode)) !== null) {
            const exportName = match[1];
            if (!usedExports.has(exportName)) {
                unusedExports.add(exportName);
            }
        }
    }

    return {
        getUnusedExports() {
            return Array.from(unusedExports);
        },
        
        analyzeFile(sourceCode) {
            scanImports(sourceCode);
            scanExports(sourceCode);
        }
    };
}

🖼️ Image Optimizer

class ImageOptimizer {
    constructor(options = {}) {
        this.options = {
            quality: options.quality || 0.8,
            maxWidth: options.maxWidth || 1200,
            format: options.format || 'webp',
            ...options
        };
    }

    async optimize(file) {
        const bitmap = await createImageBitmap(file);
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        // Redimensionar si es necesario
        let { width, height } = bitmap;
        if (width > this.options.maxWidth) {
            const ratio = this.options.maxWidth / width;
            width = this.options.maxWidth;
            height = height * ratio;
        }

        canvas.width = width;
        canvas.height = height;
        ctx.drawImage(bitmap, 0, 0, width, height);

        // Convertir a formato optimizado
        return new Promise((resolve) => {
            canvas.toBlob(
                (blob) => resolve(blob),
                `image/${this.options.format}`,
                this.options.quality
            );
        });
    }

    async processMultiple(files) {
        const results = [];
        
        for (const file of files) {
            try {
                const optimized = await this.optimize(file);
                results.push({
                    original: file,
                    optimized,
                    success: true
                });
            } catch (error) {
                results.push({
                    original: file,
                    error,
                    success: false
                });
            }
        }

        return results;
    }
}

📊 Performance Dashboard

class PerformanceDashboard {
    constructor(container) {
        this.container = container;
        this.metrics = new PerformanceMetrics();
        this.charts = new Map();
        this.setup();
    }

    setup() {
        this.createLayout();
        this.initializeCharts();
        this.startMonitoring();
    }

    createLayout() {
        this.container.innerHTML = `
            <div class="dashboard-grid">
                <div class="metric-card" id="lcp-chart">
                    <h3>Largest Contentful Paint</h3>
                    <div class="chart"></div>
                </div>
                <div class="metric-card" id="fid-chart">
                    <h3>First Input Delay</h3>
                    <div class="chart"></div>
                </div>
                <div class="metric-card" id="cls-chart">
                    <h3>Cumulative Layout Shift</h3>
                    <div class="chart"></div>
                </div>
                <div class="metric-card" id="memory-chart">
                    <h3>Memory Usage</h3>
                    <div class="chart"></div>
                </div>
            </div>
        `;
    }

    initializeCharts() {
        ['LCP', 'FID', 'CLS', 'Memory'].forEach(metric => {
            const chartContainer = this.container
                .querySelector(`#${metric.toLowerCase()}-chart .chart`);
            
            this.charts.set(metric, this.createChart(chartContainer, {
                title: metric,
                yAxis: {
                    title: this.getMetricUnit(metric)
                }
            }));
        });
    }

    getMetricUnit(metric) {
        switch (metric) {
            case 'LCP':
            case 'FID':
                return 'milliseconds';
            case 'CLS':
                return 'score';
            case 'Memory':
                return 'MB';
            default:
                return '';
        }
    }

    startMonitoring() {
        this.metrics.observe();
        this.metrics.addObserver((metrics) => {
            this.updateCharts(metrics);
        });

        // Monitorear memoria
        setInterval(() => {
            const memory = performance.memory;
            this.updateMemoryChart(memory);
        }, 1000);
    }

    updateCharts(metrics) {
        Object.entries(metrics).forEach(([metric, value]) => {
            const chart = this.charts.get(metric);
            if (chart) {
                this.updateChartData(chart, value);
            }
        });
    }

    updateMemoryChart(memory) {
        const usedHeap = memory.usedJSHeapSize / 1024 / 1024;
        const chart = this.charts.get('Memory');
        this.updateChartData(chart, usedHeap);
    }

    updateChartData(chart, value) {
        const now = new Date();
        chart.data.push({
            timestamp: now,
            value: value
        });

        // Mantener solo los últimos 60 segundos
        const sixtySecondsAgo = now - 60000;
        chart.data = chart.data.filter(point => 
            point.timestamp > sixtySecondsAgo
        );

        this.renderChart(chart);
    }

    createChart(container, options) {
        return {
            container,
            options,
            data: []
        };
    }

    renderChart(chart) {
        // Implementar visualización del gráfico
        // (usando librería de gráficos como Chart.js)
    }
}

// Estilos
const styles = `
.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1rem;
    padding: 1rem;
}

.metric-card {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    padding: 1rem;
}

.metric-card h3 {
    margin: 0 0 1rem 0;
    font-size: 1.1rem;
    color: #333;
}

.chart {
    height: 200px;
    width: 100%;
}
`;

// Uso
const dashboard = new PerformanceDashboard(

Módulo 18: Seguridad Avanzada

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-17
  • Conocimiento de criptografía básica
  • Entender protocolos de seguridad web
  • Familiaridad con tokens y autenticación

🎯 Objetivos del Módulo

  • Implementar autenticación avanzada
  • Proteger contra ataques comunes
  • Manejar datos sensibles de forma segura
  • Aplicar mejores prácticas de seguridad

📖 Autenticación Avanzada

JWT Manager

class JWTManager {
    constructor(secretKey) {
        this.secretKey = secretKey;
        this.algorithm = 'HS256';
    }

    async sign(payload, expiresIn = '1h') {
        const header = {
            alg: this.algorithm,
            typ: 'JWT'
        };

        const now = Math.floor(Date.now() / 1000);
        const exp = now + this.parseExpiration(expiresIn);

        const finalPayload = {
            ...payload,
            iat: now,
            exp
        };

        const base64Header = this.base64URLEncode(JSON.stringify(header));
        const base64Payload = this.base64URLEncode(JSON.stringify(finalPayload));

        const signature = await this.createSignature(
            `${base64Header}.${base64Payload}`
        );

        return `${base64Header}.${base64Payload}.${signature}`;
    }

    async verify(token) {
        try {
            const [headerB64, payloadB64, signatureB64] = token.split('.');

            const header = JSON.parse(this.base64URLDecode(headerB64));
            const payload = JSON.parse(this.base64URLDecode(payloadB64));

            // Verificar firma
            const expectedSignature = await this.createSignature(
                `${headerB64}.${payloadB64}`
            );

            if (signatureB64 !== expectedSignature) {
                throw new Error('Invalid signature');
            }

            // Verificar expiración
            if (payload.exp && Date.now() >= payload.exp * 1000) {
                throw new Error('Token expired');
            }

            return payload;
        } catch (error) {
            throw new Error('Invalid token');
        }
    }

    async createSignature(data) {
        const encoder = new TextEncoder();
        const key = await crypto.subtle.importKey(
            'raw',
            encoder.encode(this.secretKey),
            { name: 'HMAC', hash: 'SHA-256' },
            false,
            ['sign']
        );

        const signature = await crypto.subtle.sign(
            'HMAC',
            key,
            encoder.encode(data)
        );

        return this.arrayBufferToBase64URL(signature);
    }

    parseExpiration(expiresIn) {
        const match = expiresIn.match(/^(\d+)([smhd])$/);
        if (!match) throw new Error('Invalid expiration format');

        const [, value, unit] = match;
        const multipliers = {
            s: 1,
            m: 60,
            h: 3600,
            d: 86400
        };

        return parseInt(value) * multipliers[unit];
    }

    base64URLEncode(str) {
        return btoa(str)
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }

    base64URLDecode(str) {
        str = str.replace(/-/g, '+').replace(/_/g, '/');
        while (str.length % 4) str += '=';
        return atob(str);
    }

    arrayBufferToBase64URL(buffer) {
        return btoa(String.fromCharCode(...new Uint8Array(buffer)))
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
}

📖 Encriptación de Datos

Crypto Helper

class CryptoHelper {
    static async generateKey(password, salt) {
        const encoder = new TextEncoder();
        const passwordBuffer = encoder.encode(password);
        const saltBuffer = encoder.encode(salt);

        const key = await crypto.subtle.importKey(
            'raw',
            passwordBuffer,
            'PBKDF2',
            false,
            ['deriveBits', 'deriveKey']
        );

        return crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                salt: saltBuffer,
                iterations: 100000,
                hash: 'SHA-256'
            },
            key,
            { name: 'AES-GCM', length: 256 },
            true,
            ['encrypt', 'decrypt']
        );
    }

    static async encrypt(data, key) {
        const encoder = new TextEncoder();
        const iv = crypto.getRandomValues(new Uint8Array(12));

        const encrypted = await crypto.subtle.encrypt(
            {
                name: 'AES-GCM',
                iv
            },
            key,
            encoder.encode(JSON.stringify(data))
        );

        return {
            iv: Array.from(iv),
            data: Array.from(new Uint8Array(encrypted))
        };
    }

    static async decrypt(encryptedData, key) {
        const { iv, data } = encryptedData;
        
        const decrypted = await crypto.subtle.decrypt(
            {
                name: 'AES-GCM',
                iv: new Uint8Array(iv)
            },
            key,
            new Uint8Array(data)
        );

        const decoder = new TextDecoder();
        return JSON.parse(decoder.decode(decrypted));
    }
}

📖 Protección contra Ataques

Security Headers Manager

class SecurityHeaders {
    static getHeaders() {
        return {
            'Content-Security-Policy': this.getCSP(),
            'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
            'X-Content-Type-Options': 'nosniff',
            'X-Frame-Options': 'DENY',
            'X-XSS-Protection': '1; mode=block',
            'Referrer-Policy': 'strict-origin-when-cross-origin',
            'Permissions-Policy': this.getPermissionsPolicy()
        };
    }

    static getCSP() {
        return [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self'",
            "connect-src 'self' https:",
            "media-src 'self'",
            "object-src 'none'",
            "frame-ancestors 'none'",
            "base-uri 'self'",
            "form-action 'self'"
        ].join('; ');
    }

    static getPermissionsPolicy() {
        return [
            'camera=()',
            'microphone=()',
            'geolocation=()',
            'payment=()',
            'usb=()',
            'fullscreen=(self)'
        ].join(', ');
    }
}

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: Sistema de Autenticación 2FA

class TwoFactorAuth {
    constructor() {
        this.secretLength = 20;
        this.codeLength = 6;
        this.timestep = 30;
    }

    generateSecret() {
        const bytes = crypto.getRandomValues(new Uint8Array(this.secretLength));
        return this.base32Encode(bytes);
    }

    generateTOTP(secret, time = Date.now()) {
        const counter = Math.floor(time / 1000 / this.timestep);
        return this.generateHOTP(secret, counter);
    }

    async generateHOTP(secret, counter) {
        const key = await this.importKey(this.base32Decode(secret));
        const counterBytes = new Uint8Array(8);
        for (let i = 7; i >= 0; i--) {
            counterBytes[i] = counter & 0xff;
            counter >>= 8;
        }

        const signature = await crypto.subtle.sign(
            'HMAC',
            key,
            counterBytes
        );

        const offset = new Uint8Array(signature)[19] & 0xf;
        const code = (
            ((signature[offset] & 0x7f) << 24) |
            ((signature[offset + 1] & 0xff) << 16) |
            ((signature[offset + 2] & 0xff) << 8) |
            (signature[offset + 3] & 0xff)
        ) % Math.pow(10, this.codeLength);

        return code.toString().padStart(this.codeLength, '0');
    }

    async verifyToken(token, secret) {
        const now = Date.now();
        // Verificar token actual y tokens adyacentes
        for (let i = -1; i <= 1; i++) {
            const generatedToken = await this.generateTOTP(
                secret,
                now + i * this.timestep * 1000
            );
            if (token === generatedToken) return true;
        }
        return false;
    }

    // Utilidades de codificación
    base32Encode(bytes) {
        const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        let bits = 0;
        let value = 0;
        let output = '';

        for (let i = 0; i < bytes.length; i++) {
            value = (value << 8) | bytes[i];
            bits += 8;

            while (bits >= 5) {
                output += alphabet[(value >>> (bits - 5)) & 31];
                bits -= 5;
            }
        }

        if (bits > 0) {
            output += alphabet[(value << (5 - bits)) & 31];
        }

        return output;
    }

    base32Decode(str) {
        const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        str = str.toUpperCase();
        let bits = 0;
        let value = 0;
        const output = new Uint8Array(str.length * 5 / 8 | 0);
        let index = 0;

        for (let i = 0; i < str.length; i++) {
            value = (value << 5) | alphabet.indexOf(str[i]);
            bits += 5;

            if (bits >= 8) {
                output[index++] = (value >>> (bits - 8)) & 255;
                bits -= 8;
            }
        }

        return output;
    }

    async importKey(secret) {
        return crypto.subtle.importKey(
            'raw',
            secret,
            { name: 'HMAC', hash: 'SHA-1' },
            false,
            ['sign']
        );
    }
}

💡 Mini Proyecto: Sistema de Autenticación Seguro

class SecureAuthSystem {
    constructor() {
        this.jwtManager = new JWTManager(process.env.JWT_SECRET);
        this.twoFactorAuth = new TwoFactorAuth();
        this.cryptoHelper = CryptoHelper;
    }

    async register(username, password) {
        // Generar salt único
        const salt = crypto.getRandomValues(new Uint8Array(16));
        
        // Derivar clave de encriptación
        const key = await this.cryptoHelper.generateKey(password, salt);
        
        // Generar secreto 2FA
        const twoFactorSecret = this.twoFactorAuth.generateSecret();
        
        // Encriptar datos sensibles
        const encryptedData = await this.cryptoHelper.encrypt({
            twoFactorSecret,
            createdAt: Date.now()
        }, key);

        // Guardar usuario en base de datos
        const user = {
            username,
            salt: Array.from(salt),
            encryptedData,
            enabled2FA: false
        };

        // Retornar secreto 2FA para configuración
        return {
            user,
            twoFactorSecret
        };
    }

    async login(username, password, twoFactorToken = null) {
        // Obtener usuario de la base de datos
        const user = await this.getUser(username);
        if (!user) throw new Error('Usuario no encontrado');

        // Verificar contraseña y obtener datos
        const key = await this.cryptoHelper.generateKey(password, new Uint8Array(user.salt));
        const userData = await this.cryptoHelper.decrypt(user.encryptedData, key);

        // Verificar 2FA si está habilitado
        if (user.enabled2FA) {
            if (!twoFactorToken) {
                throw new Error('Se requiere token 2FA');
            }

            const isValidToken = await this.twoFactorAuth.verifyToken(
                twoFactorToken,
                userData.twoFactorSecret
            );

            if (!isValidToken) {
                throw new Error('Token 2FA inválido');
            }
        }

        // Generar token de sesión
        const token = await this.jwtManager.sign({
            sub: username,
            type: 'access'
        });

        // Generar token de refresco
        const refreshToken = await this.jwtManager.sign({
            sub: username,
            type: 'refresh'
        }, '7d');

        return {
            accessToken: token,
            refreshToken,
            user: {
                username,
                enabled2FA: user.enabled2FA
            }
        };
    }

    async refreshToken(refreshToken) {
        try {
            const payload = await this.jwtManager.verify(refreshToken);
            
            if (payload.type !== 'refresh') {
                throw new Error('Token inválido');
            }

            const newToken = await this.jwtManager.sign({
                sub: payload.sub,
                type: 'access'
            });

            return { accessToken: newToken };
        } catch (error) {
            throw new Error('Token de refresco inválido');
        }
    }

    async enable2FA(username, token) {
        const user = await this.getUser(username);
        if (!user) throw new Error('Usuario no encontrado');

        // Verificar token 2FA
        const isValidToken = await this.twoFactorAuth.verifyToken(
            token,
            user.twoFactorSecret
        );

        if (!isValidToken) {
            throw new Error('Token 2FA inválido');
        }

        // Actualizar usuario
        user.enabled2FA = true;
        // Guardar en base de datos

        return { success: true };
    }
}

// Estilos CSS para la interfaz
const styles = `
.auth-container {
    max-width: 400px;
    margin: 2rem auto;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    background: white;
}

.auth-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.auth-input {
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}

.auth-button {
    padding: 0.75rem;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    cursor: pointer;
    transition: background 0.2s;
}

.auth-button:hover {
    background: #0056b3;
}

.auth-error {
    color: #dc3545;
    padding: 0.5rem;
    margin-top: 0.5rem;
    border-radius: 4px;
    background: #f8d7da;
}

.qr-container {
    text-align: center;
    margin: 1rem 0;
}

.qr-code {
    padding: 1rem;
    background: white;
    border-radius: 4px;
    display: inline-block;
}
`;

// Uso del sistema
const authSystem = new SecureAuthSystem();

Módulo 19: Microservicios y Arquitecturas Distribuidas

⏱️ Tiempo estimado: 10-12 horas

📋 Prerequisites

  • Completar módulos 1-18
  • Conocimiento de arquitecturas distribuidas
  • Familiaridad con Docker y contenedores
  • Entender comunicación entre servicios

🎯 Objetivos del Módulo

  • Implementar arquitecturas de microservicios
  • Gestionar comunicación entre servicios
  • Manejar fallos distribuidos
  • Aplicar patrones de resiliencia

📖 Arquitectura de Microservicios

Service Registry

class ServiceRegistry {
    constructor() {
        this.services = new Map();
        this.healthChecks = new Map();
    }

    register(serviceName, instance) {
        if (!this.services.has(serviceName)) {
            this.services.set(serviceName, new Set());
        }
        this.services.get(serviceName).add(instance);
        this.setupHealthCheck(serviceName, instance);
    }

    deregister(serviceName, instance) {
        const instances = this.services.get(serviceName);
        if (instances) {
            instances.delete(instance);
            this.cleanupHealthCheck(serviceName, instance);
        }
    }

    getInstance(serviceName) {
        const instances = Array.from(this.services.get(serviceName) || []);
        if (!instances.length) {
            throw new Error(`No instances available for ${serviceName}`);
        }
        // Round-robin simple
        return instances[Math.floor(Math.random() * instances.length)];
    }

    setupHealthCheck(serviceName, instance) {
        const check = setInterval(async () => {
            try {
                const response = await fetch(`${instance}/health`);
                if (!response.ok) {
                    this.deregister(serviceName, instance);
                }
            } catch {
                this.deregister(serviceName, instance);
            }
        }, 30000);

        this.healthChecks.set(`${serviceName}-${instance}`, check);
    }

    cleanupHealthCheck(serviceName, instance) {
        const key = `${serviceName}-${instance}`;
        clearInterval(this.healthChecks.get(key));
        this.healthChecks.delete(key);
    }
}

Circuit Breaker

class CircuitBreaker {
    constructor(fn, options = {}) {
        this.fn = fn;
        this.options = {
            failureThreshold: options.failureThreshold || 5,
            resetTimeout: options.resetTimeout || 60000,
            ...options
        };

        this.state = 'CLOSED';
        this.failures = 0;
        this.lastFailureTime = null;
    }

    async execute(...args) {
        if (this.state === 'OPEN') {
            if (Date.now() - this.lastFailureTime >= this.options.resetTimeout) {
                this.state = 'HALF-OPEN';
            } else {
                throw new Error('Circuit breaker is OPEN');
            }
        }

        try {
            const result = await this.fn(...args);
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }

    onSuccess() {
        this.failures = 0;
        this.state = 'CLOSED';
    }

    onFailure() {
        this.failures++;
        this.lastFailureTime = Date.now();

        if (this.failures >= this.options.failureThreshold) {
            this.state = 'OPEN';
        }
    }
}

📖 Patrones de Comunicación

Event Bus Distribuido

class DistributedEventBus {
    constructor(options = {}) {
        this.subscribers = new Map();
        this.retryAttempts = options.retryAttempts || 3;
        this.retryDelay = options.retryDelay || 1000;
    }

    async publish(topic, message) {
        const subscribers = this.subscribers.get(topic) || [];
        const promises = subscribers.map(subscriber =>
            this.deliverWithRetry(subscriber, message)
        );

        return Promise.allSettled(promises);
    }

    subscribe(topic, handler) {
        if (!this.subscribers.has(topic)) {
            this.subscribers.set(topic, []);
        }
        this.subscribers.get(topic).push(handler);

        return () => {
            const subs = this.subscribers.get(topic);
            const index = subs.indexOf(handler);
            if (index > -1) {
                subs.splice(index, 1);
            }
        };
    }

    async deliverWithRetry(handler, message, attempt = 1) {
        try {
            await handler(message);
        } catch (error) {
            if (attempt < this.retryAttempts) {
                await new Promise(resolve => 
                    setTimeout(resolve, this.retryDelay * attempt)
                );
                return this.deliverWithRetry(handler, message, attempt + 1);
            }
            throw error;
        }
    }
}

🏋️‍♂️ Ejercicios Prácticos

Ejercicio 1: API Gateway

class APIGateway {
    constructor(serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
        this.routeHandlers = new Map();
        this.circuitBreakers = new Map();
    }

    registerRoute(path, serviceName, options = {}) {
        const handler = async (req) => {
            const instance = this.serviceRegistry.getInstance(serviceName);
            const breaker = this.getCircuitBreaker(serviceName);

            return breaker.execute(() =>
                fetch(`${instance}${path}`, {
                    method: req.method,
                    headers: req.headers,
                    body: req.body
                })
            );
        };

        this.routeHandlers.set(path, handler);
    }

    getCircuitBreaker(serviceName) {
        if (!this.circuitBreakers.has(serviceName)) {
            this.circuitBreakers.set(
                serviceName,
                new CircuitBreaker(
                    (fn) => fn(),
                    { failureThreshold: 3, resetTimeout: 30000 }
                )
            );
        }
        return this.circuitBreakers.get(serviceName);
    }

    async handleRequest(req) {
        const handler = this.findHandler(req.path);
        if (!handler) {
            return {
                status: 404,
                body: { error: 'Not Found' }
            };
        }

        try {
            const response = await handler(req);
            return {
                status: response.status,
                headers: response.headers,
                body: await response.json()
            };
        } catch (error) {
            return {
                status: 500,
                body: { error: 'Service Unavailable' }
            };
        }
    }

    findHandler(path) {
        // Implementar búsqueda de rutas con patrones
        return this.routeHandlers.get(path);
    }
}

💡 Mini Proyecto: Sistema de Microservicios

// Implementación de un sistema de microservicios básico
class MicroserviceSystem {
    constructor() {
        this.serviceRegistry = new ServiceRegistry();
        this.eventBus = new DistributedEventBus();
        this.gateway = new APIGateway(this.serviceRegistry);
        this.setupServices();
    }

    setupServices() {
        // Configurar rutas en el gateway
        this.gateway.registerRoute(
            '/users',
            'user-service',
            { timeout: 5000 }
        );

        this.gateway.registerRoute(
            '/orders',
            'order-service',
            { timeout: 10000 }
        );

        // Suscribirse a eventos
        this.eventBus.subscribe('order.created', async (order) => {
            await this.notifyUser(order);
            await this.updateInventory(order);
        });
    }

    async notifyUser(order) {
        const userService = this.serviceRegistry
            .getInstance('user-service');

        await fetch(`${userService}/notifications`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                userId: order.userId,
                message: `Order ${order.id} created`
            })
        });
    }

    async updateInventory(order) {
        const inventoryService = this.serviceRegistry
            .getInstance('inventory-service');

        await fetch(`${inventoryService}/update-stock`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(order.items)
        });
    }

    async createOrder(orderData) {
        try {
            // Crear orden
            const response = await this.gateway
                .handleRequest({
                    path: '/orders',
                    method: 'POST',
                    body: orderData
                });

            if (response.status === 201) {
                // Publicar evento
                await this.eventBus.publish(
                    'order.created',
                    response.body
                );
                return response.body;
            }

            throw new Error('Failed to create order');
        } catch (error) {
            console.error('Error creating order:', error);
            throw error;
        }
    }
}

// Ejemplo de uso
const system = new MicroserviceSystem();

// Registrar servicios
system.serviceRegistry.register(
    'user-service',
    'http://user-service:3001'
);
system.serviceRegistry.register(
    'order-service',
    'http://order-service:3002'
);
system.serviceRegistry.register(
    'inventory-service',
    'http://inventory-service:3003'
);

// Crear orden
system.createOrder({
    userId: 1,
    items: [
        { productId: 1, quantity: 2 },
        { productId: 2, quantity: 1 }
    ]
}).then(order => {
    console.log('Order created:', order);
}).catch(error => {
    console.error('Error creating order:', error);
});