# Frontend Avanzado — Ejemplos Completos de APIs Nativas

Implementaciones completas de Web Workers, Service Workers, WebSockets, SSE, IndexedDB,
Container Queries, CSS Layers y View Transitions.

---

## Web Workers — Comunicación Tipada Completa

```typescript
// estadisticas.worker.ts
/// <reference lib="webworker" />

interface MensajeEntrada {
  tipo: "CALCULAR";
  datos: number[];
  id: string;
}

interface MensajeSalida {
  tipo: "RESULTADO" | "ERROR" | "PROGRESO";
  id: string;
  payload: EstadisticasResultado | string | number;
}

self.addEventListener("message", (event: MessageEvent<MensajeEntrada>) => {
  const { tipo, datos, id } = event.data;

  if (tipo === "CALCULAR") {
    try {
      const total = datos.length;
      const correlaciones: number[] = [];

      for (let i = 0; i < total; i++) {
        for (let j = i + 1; j < total; j++) {
          correlaciones.push(calcularCorrelacion(datos[i], datos[j]));
        }
        if (i % Math.floor(total / 10) === 0) {
          self.postMessage({
            tipo: "PROGRESO", id,
            payload: Math.round((i / total) * 100),
          } satisfies MensajeSalida);
        }
      }

      self.postMessage({
        tipo: "RESULTADO", id,
        payload: { correlaciones, promedio: datos.reduce((s, n) => s + n, 0) / total },
      } satisfies MensajeSalida);
    } catch (e) {
      self.postMessage({ tipo: "ERROR", id, payload: String(e) } satisfies MensajeSalida);
    }
  }
});

// servicio-worker.service.ts (Angular)
@Injectable({ providedIn: "root" })
export class ServicioEstadisticas {
  private worker = new Worker(
    new URL("../workers/estadisticas.worker", import.meta.url),
    { type: "module" }
  );

  calcular(datos: number[]): Observable<EstadisticasResultado> {
    const id = crypto.randomUUID();

    return new Observable(observer => {
      const manejador = (event: MessageEvent<MensajeSalida>) => {
        if (event.data.id !== id) return;
        switch (event.data.tipo) {
          case "PROGRESO": break;
          case "RESULTADO":
            observer.next(event.data.payload as EstadisticasResultado);
            observer.complete();
            this.worker.removeEventListener("message", manejador);
            break;
          case "ERROR":
            observer.error(new Error(event.data.payload as string));
            this.worker.removeEventListener("message", manejador);
            break;
        }
      };
      this.worker.addEventListener("message", manejador);
      this.worker.postMessage({ tipo: "CALCULAR", datos, id });
      return () => this.worker.removeEventListener("message", manejador);
    });
  }
}
```

---

## Service Workers — Estrategias de Cache

```typescript
// service-worker.ts
const CACHE_VERSION = "v3";
const CACHE_ESTATICO = `estatico-${CACHE_VERSION}`;
const CACHE_DINAMICO = `dinamico-${CACHE_VERSION}`;

const RECURSOS_PRECACHEADOS = ["/", "/manifest.json", "/offline.html"];

self.addEventListener("install", (event: ExtendableEvent) => {
  event.waitUntil(
    caches.open(CACHE_ESTATICO).then(cache => cache.addAll(RECURSOS_PRECACHEADOS))
  );
  (self as ServiceWorkerGlobalScope).skipWaiting();
});

self.addEventListener("activate", (event: ExtendableEvent) => {
  event.waitUntil(
    caches.keys().then(claves =>
      Promise.all(
        claves
          .filter(c => c !== CACHE_ESTATICO && c !== CACHE_DINAMICO)
          .map(c => caches.delete(c))
      )
    )
  );
  (self as ServiceWorkerGlobalScope).clients.claim();
});

self.addEventListener("fetch", (event: FetchEvent) => {
  const { request } = event;
  const url = new URL(request.url);

  if (url.pathname.startsWith("/api/")) {
    event.respondWith(networkFirst(request));
    return;
  }
  if (request.destination === "image" || url.pathname.endsWith(".js")) {
    event.respondWith(cacheFirst(request));
    return;
  }
  event.respondWith(staleWhileRevalidate(request));
});

async function networkFirst(request: Request): Promise<Response> {
  try {
    const respuesta = await fetch(request.clone());
    const cache = await caches.open(CACHE_DINAMICO);
    cache.put(request, respuesta.clone());
    return respuesta;
  } catch {
    const cached = await caches.match(request);
    return cached ?? caches.match("/offline.html") as Promise<Response>;
  }
}

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) return cached;
  const respuesta = await fetch(request);
  const cache = await caches.open(CACHE_DINAMICO);
  cache.put(request, respuesta.clone());
  return respuesta;
}

async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open(CACHE_DINAMICO);
  const cached = await caches.match(request);
  const fetchPromise = fetch(request).then(respuesta => {
    cache.put(request, respuesta.clone());
    return respuesta;
  });
  return cached ?? fetchPromise;
}
```

---

## WebSockets — Conexión Robusta con Reconexión

```typescript
@Injectable({ providedIn: "root" })
export class WebSocketService {
  private socket: WebSocket | null = null;
  private reintentos = 0;
  private readonly MAX_REINTENTOS = 5;
  private readonly DELAY_BASE_MS = 1000;
  private readonly _mensajes$ = new Subject<MensajeWS>();
  private readonly _estado$ = new BehaviorSubject<"conectando" | "conectado" | "desconectado">("desconectado");
  readonly mensajes$ = this._mensajes$.asObservable();
  readonly estado$   = this._estado$.asObservable();

  conectar(url: string): void {
    this._estado$.next("conectando");
    this.socket = new WebSocket(url);
    this.socket.onopen = () => {
      this.reintentos = 0;
      this._estado$.next("conectado");
    };
    this.socket.onmessage = (event: MessageEvent) => {
      try {
        this._mensajes$.next(JSON.parse(event.data) as MensajeWS);
      } catch { console.warn("Mensaje WS no parseado:", event.data); }
    };
    this.socket.onclose = (event: CloseEvent) => {
      this._estado$.next("desconectado");
      if (event.wasClean || this.reintentos >= this.MAX_REINTENTOS) return;
      const delay = this.DELAY_BASE_MS * 2 ** this.reintentos;
      this.reintentos++;
      setTimeout(() => this.conectar(url), delay);
    };
    this.socket.onerror = () => {};
  }

  enviar<T>(tipo: string, payload: T): void {
    if (this.socket?.readyState !== WebSocket.OPEN) throw new Error("WebSocket no conectado");
    this.socket.send(JSON.stringify({ tipo, payload, timestamp: Date.now() }));
  }

  desconectar(): void {
    this.reintentos = this.MAX_REINTENTOS;
    this.socket?.close(1000, "Cierre normal");
  }
}
```

---

## SSE — Server-Sent Events

```typescript
@Injectable({ providedIn: "root" })
export class SSEService {
  escuchar<T>(url: string): Observable<T> {
    return new Observable<T>(observer => {
      const fuente = new EventSource(url, { withCredentials: true });
      fuente.onmessage = (event: MessageEvent) => {
        try { observer.next(JSON.parse(event.data) as T); }
        catch { observer.error(new Error(`SSE: JSON inválido: ${event.data}`)); }
      };
      fuente.onerror = () => observer.error(new Error("Conexión SSE perdida"));
      return () => fuente.close();
    }).pipe(
      retryWhen(errores =>
        errores.pipe(
          delayWhen((_, intento) => timer(Math.min(1000 * 2 ** intento, 30000)))
        )
      )
    );
  }
}
```

---

## IndexedDB — Almacenamiento Estructurado

```typescript
import { openDB, DBSchema, IDBPDatabase } from "idb";

interface EsquemaBD extends DBSchema {
  pedidos: {
    key: string;
    value: { id: string; datos: object; sincronizado: boolean; timestamp: number };
    indexes: { "por-sincronizado": boolean };
  };
  cache_respuestas: {
    key: string;
    value: { url: string; datos: unknown; expira: number };
  };
}

@Injectable({ providedIn: "root" })
export class AlmacenamientoLocalService {
  private db!: IDBPDatabase<EsquemaBD>;

  async inicializar(): Promise<void> {
    this.db = await openDB<EsquemaBD>("mi-app-db", 2, {
      upgrade(db, versionAnterior) {
        if (versionAnterior < 1) {
          const storePedidos = db.createObjectStore("pedidos", { keyPath: "id" });
          storePedidos.createIndex("por-sincronizado", "sincronizado");
          db.createObjectStore("cache_respuestas", { keyPath: "url" });
        }
      },
    });
  }

  async guardarPedidoOffline(pedido: object & { id: string }): Promise<void> {
    await this.db.put("pedidos", {
      id: pedido.id, datos: pedido, sincronizado: false, timestamp: Date.now(),
    });
  }

  async sincronizarPendientes(): Promise<void> {
    const pendientes = await this.db.getAllFromIndex("pedidos", "por-sincronizado", false);
    for (const pendiente of pendientes) {
      try {
        await fetch("/api/pedidos", {
          method: "POST", body: JSON.stringify(pendiente.datos),
          headers: { "Content-Type": "application/json" },
        });
        await this.db.put("pedidos", { ...pendiente, sincronizado: true });
      } catch { /* Sin red — reintentará */ }
    }
  }
}
```

---

## View Transitions API

```typescript
@Injectable({ providedIn: "root" })
export class TransicionesService {
  private router = inject(Router);

  inicializarTransiciones(): void {
    if (!("startViewTransition" in document)) return;
    this.router.events
      .pipe(filter(e => e instanceof NavigationStart))
      .subscribe(() => {
        (document as any).startViewTransition(() =>
          new Promise<void>(resolve => {
            const sub = this.router.events
              .pipe(filter(e => e instanceof NavigationEnd), take(1))
              .subscribe(() => { sub.unsubscribe(); resolve(); });
          })
        );
      });
  }
}
```

```css
::view-transition-old(root) {
  animation: desvanece-sale 0.3s ease-out;
}
::view-transition-new(root) {
  animation: desvanece-entra 0.3s ease-in;
}
.tarjeta-detalle {
  view-transition-name: tarjeta-activa;
}
@keyframes desvanece-sale  { to   { opacity: 0; transform: translateX(-20px); } }
@keyframes desvanece-entra { from { opacity: 0; transform: translateX( 20px); } }
```
