# Cache de Navegación — Diseño, Decisiones y Rendimiento

## El problema que resuelve

El menú de navegación se genera leyendo todos los archivos `pages.json` de cada módulo,
filtrando por permisos del usuario, y agrupando el resultado. Con 45 módulos eso implica
45 lecturas de disco + decode JSON + pipeline de colecciones en **cada page load**.

Sin cache: a 100 usuarios × 10 requests/minuto = 45,000 operaciones I/O por minuto
solo para renderizar el menú lateral.

---

## Diseño elegido: cache por versión + observers

### Por qué no TTL puro

Un TTL fijo (ej. 10 minutos) tiene dos problemas opuestos:
- Regenera innecesariamente aunque nada haya cambiado
- Tarda hasta X minutos en reflejar un cambio real de permisos

El TTL es básicamente admitir que no sabes cuándo cambia algo. Si sí lo sabes, el TTL
es ruido.

### El enfoque adoptado

**Una clave global en cache (`permissions_version`) actúa como versión del sistema.**
Cada vez que algo cambia (permiso creado, rol modificado, módulo activado), esa versión
sube. El menú de cada usuario almacena la versión con la que fue generado. En cada
request se comparan ambas versiones — si coinciden, se sirve el cache; si no, se regenera.

```
Request normal (happy path):
  1. Lee permissions_version  →  1 cache read, sub-ms
  2. Lee user_{id}_menu       →  1 cache read, sub-ms
  3. Versiones coinciden      →  renueva TTL → retorna menú
  Costo total: ~0.2ms

Request post-cambio de permisos:
  1. Lee permissions_version  →  versión nueva
  2. Lee user_{id}_menu       →  versión vieja → mismatch
  3. Adquiere lock por usuario
  4. Genera menú              →  ~5ms
  5. Guarda en cache          →  renueva versión y TTL
  6. Libera lock → retorna menú
  Costo total: ~6ms (solo en el primer request post-cambio)
```

---

## Componentes del sistema

### PermissionVersion — helper centralizado

`app/Helpers/PermissionVersion.php`

Único lugar que escribe `permissions_version`. Nadie más toca esa clave directamente.

```php
PermissionVersion::bump(); // sube la versión global
```

### Observers automáticos

Registrados en `AppServiceProvider`. Cubren cualquier cambio de modelo desde cualquier
lugar del sistema: Livewire, comandos Artisan, seeders, imports CSV, APIs externas.
No importa el origen — el observer siempre dispara.

| Observer | Eventos |
|---|---|
| `PermissionObserver` | `created`, `deleted` → `bump()` |
| `RoleObserver` | `updated`, `deleted` → `bump()` |
| `UserObserver` | Cambio de `is_admin`/roles → `bump()` · `deleted` → borra cache del usuario |

### bump() explícito

Solo en `ModuleManager::toggle()` — activar/desactivar un módulo cambia las rutas
disponibles pero no toca modelos de Permission ni Role, así que los observers no lo
cubren.

Los métodos de `PermisoPagina` y `MenuNavegacion` están cubiertos por los observers
automáticamente. No necesitan `bump()` explícito.

### Lock por usuario

Evita que múltiples tabs del mismo usuario generen el menú simultáneamente después de
un cache miss. El lock es por `user_id`, así que 500 usuarios con cache miss simultáneo
son 500 locks independientes que no compiten entre sí.

---

## Gestión de cache basura

Sin limpieza, el cache de usuarios inactivos o eliminados se acumula indefinidamente.

| Caso | Mecanismo |
|---|---|
| Permiso o rol cambia | Observer → `bump()` → todos regeneran en su próximo request |
| Módulo activado/desactivado | `bump()` explícito en `ModuleManager` |
| Usuario eliminado | `UserObserver::deleted` → borra `user_{id}_menu` inmediatamente |
| Usuario activo | TTL de 30 días se renueva en cada visita → nunca expira en uso normal |
| Usuario inactivo | TTL de 30 días sin renovación → expira solo tras 30 días sin visitas |

**El TTL de 30 días no cumple función de invalidación** — para eso están los observers.
Su único rol es ser recolector de basura: "bórrate si este usuario no ha vuelto en 30 días".

---

## Rendimiento esperado por escala

Suposiciones: 45 módulos, ~1,470 permisos, generación de menú ~5ms, servidor LAMP/LEMP estándar.
"Post-bump" = peor caso inmediatamente después de un cambio de permisos.

| Usuarios simultáneos | Cache hit (normal) | Post-bump | Costo/request | Infraestructura recomendada |
|---|---|---|---|---|
| 1 – 5 | Sub-ms | 1 generación, resto esperan lock | Insignificante | Cualquiera, XAMPP incluido |
| 10 – 20 | Sub-ms | ~20 generaciones independientes | Insignificante | Servidor compartido básico |
| 50 | Sub-ms | ~50 generaciones independientes | < 1ms promedio | VPS pequeño |
| 100 | Sub-ms | ~100 generaciones independientes | < 1ms promedio | VPS estándar |
| 200 | Sub-ms | ~200 generaciones independientes | < 1ms promedio | VPS estándar, ojo al pool de conexiones DB |
| 500 | Sub-ms | ~500 generaciones independientes | ~1–2ms promedio | VPS mediano, file cache empieza a limitarse |
| 1,000 | Sub-ms | ~1,000 generaciones independientes | ~2–3ms promedio | **Migrar a Redis** |
| 2,000 | Sub-ms | Distribuido en minutos, no segundos | ~3ms promedio | Redis + considerar queue para regeneración |
| 5,000+ | Sub-ms | Queue procesa en background | ~2ms promedio | Redis obligatorio + queue workers |
| 10,000+ | Sub-ms | Regeneración 100% asíncrona | Sub-ms | Redis + queue + considerar cache distribuido |

### Por qué el post-bump no es tan dramático

Con 1,000 usuarios simultáneos y un bump, suena a 1,000 generaciones al mismo tiempo.
En la práctica:

- Los usuarios no recargan al mismo microsegundo
- Cada generación es independiente, no compiten por el mismo recurso
- Distribuidas en 30–60 segundos reales: ~17 generaciones/segundo
- A 5ms cada una con 8 workers PHP-FPM: completamente manejable sin Redis

El salto real de complejidad ocurre en **1,000+ usuarios simultáneos**.

---

## Cuándo y cómo migrar a Redis

### Señales de que es momento

- El cache driver `file` empieza a generar contención de I/O (se nota en tiempos de
  respuesta irregulares bajo carga)
- Tienes más de ~800 usuarios simultáneos de forma sostenida
- Necesitas compartir cache entre múltiples servidores (balanceo de carga)

### Cómo migrar

El sistema está diseñado para que la migración sea un cambio de configuración,
no de código.

**1. Instalar Redis en el servidor**
```bash
# Ubuntu/Debian
sudo apt install redis-server
sudo systemctl enable redis
```

**2. Instalar el cliente PHP**
```bash
composer require predis/predis
# o instalar la extensión phpredis
```

**3. Cambiar el driver en `.env`**
```env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```

Nada más. Todo el código de observers, locks y TTL funciona igual con Redis.
La diferencia es que Redis es ~10x más rápido que file cache y soporta operaciones
atómicas nativas (los locks son más confiables).

### Beneficio adicional con Redis: Cache Tags

Con Redis se puede habilitar una invalidación aún más limpia usando tags.
En lugar de la clave `permissions_version`, se puede hacer:

```php
// Guardar con tag
Cache::tags(['menus'])->put('user_1_menu', $data, now()->addDays(30));

// Invalidar todos los menús de golpe
Cache::tags(['menus'])->flush();
```

Esto elimina la necesidad de la clave `permissions_version` y el chequeo de versión.
Es más simple, pero requiere Redis (no funciona con file/database cache driver).

---

## Decisiones descartadas y por qué

| Opción | Por qué se descartó |
|---|---|
| TTL fijo de 10 minutos | Regenera sin razón + tarda en reflejar cambios reales |
| `Cache::forever()` sin TTL de ningún tipo | Cache basura de usuarios inactivos/eliminados crece sin límite |
| Campo fecha en DB para la versión | Una query DB extra por page load vs una lectura de cache sub-ms |
| `bump()` manual en cada método | Si se agrega un nuevo lugar que crea permisos y se olvida el `bump()`, el bug es silencioso e invisible |
| Cache tags sin Redis | No disponible con driver `file` o `database` |
