/** * Cache-Aside + Write-Through storage para Alpha. * * ─── Patrón ───────────────────────────────────────────────────── * * L1 (rápido) → Map en memoria ~0ms * L2 (persistente) → Dexie IndexedDB ~5-50ms * L3 (fallback) → localStorage ~1ms * * ─── Lectura (Cache-Aside) ───────────────────────────────────── * * get(key): * 1. L1 hit → return (instantáneo) * 2. L2 hit → poblar L1 → return * 3. L3 hit → poblar L2 + L1 → return * 4. Miss → return null * * ─── Escritura (Write-Through) ───────────────────────────────── * * set(key, value): * 1. Escribir L1 (instantáneo) * 2. Escribir L2 (Dexie, async await) * 3. Escribir L3 (localStorage, sync) * Si L2 falla → el dato sigue en L1 + L3 (consistencia eventual) * * ─── ¿Por qué este patrón? ───────────────────────────────────── * * - Cache-Aside: control explícito de qué se cachea y cuándo * - Write-Through: el cache siempre refleja la fuente de verdad * - L3 (localStorage) actúa como quorum: si IndexedDB falla, * los datos no se pierden * - Para RUMBO: mismo patrón, cambia L2 de Dexie a Turso/libSQL * y L3 pasa a ser un archivo JSON en ~/.rumbo/cache.json * * ─── Uso ─────────────────────────────────────────────────────── * * import { storage } from '@/services/storage' * await storage.init() * storage.get('key') → string | null (sync) * await storage.set('k', v) → void (async) * await storage.remove('k') → void (async) * storage.getJSON('k') → T | null (sync) * await storage.setJSON() → void (async) */ import db from '@/services/db' class AppStorage { /** L1: cache en memoria */ private cache = new Map() private loaded = false private initPromise: Promise | null = null // ─── Init ────────────────────────────────────────────────── async init() { if (this.initPromise) return this.initPromise this.initPromise = this._load() return this.initPromise } private async _load() { const all = await db.settings.toArray() for (const entry of all) { this.cache.set(entry.key, { value: entry.value, ttl: null }) } this.loaded = true console.log(`[Storage] Init OK — ${all.length} entries en L1`) } isReady() { return this.loaded } // ─── Lectura: Cache-Aside (L1 → L2 → L3) ──────────────── get(key: string): string | null { // 1. L1 hit const entry = this.cache.get(key) if (entry) { if (entry.ttl !== null && entry.ttl < Date.now()) { this.cache.delete(key) } else { return entry.value } } // 2. L2 (Dexie) // Nota: Dexie.get() es async. Para mantener get() sync, // delegamos la carga asíncrona a init(). Si no está en L1 // después de init, cae a L3. // En la práctica, init() carga TODO al arranque, así que // L1 siempre está poblado para keys existentes. // 3. L3 (localStorage) — fallback + migración const fallback = localStorage.getItem(key) if (fallback !== null) { this.cache.set(key, { value: fallback, ttl: null }) // Write-Through hacia L2 (fire-and-forget) db.settings.put({ key, value: fallback }).catch(() => {}) return fallback } return null } getJSON(key: string): T | null { const raw = this.get(key) if (!raw) return null try { return JSON.parse(raw) as T } catch { return null } } // ─── Escritura: Write-Through (L1 + L2 + L3) ───────────── async set(key: string, value: string) { // 1. L1 (instantáneo) this.cache.set(key, { value, ttl: null }) // 2. L2 + L3 en paralelo await Promise.all([ db.settings.put({ key, value }).catch(e => { console.error(`[Storage] L2 error writing "${key}":`, e) }), Promise.resolve().then(() => { localStorage.setItem(key, value) }), ]) } async setJSON(key: string, value: unknown) { await this.set(key, JSON.stringify(value)) } async remove(key: string) { this.cache.delete(key) await Promise.all([ db.settings.delete(key).catch(e => { console.error(`[Storage] L2 error deleting "${key}":`, e) }), Promise.resolve().then(() => { localStorage.removeItem(key) }), ]) } // ─── Utilidades ─────────────────────────────────────────── /** Expone el tamaño del cache L1 (debug) */ get cacheSize() { return this.cache.size } } export const storage = new AppStorage()