Files
Alpha/src/services/storage.ts
T
ricardo 7d299554bf K-10 pipeline transcripciones + settings IA + cache-aside + session doc
Nuevos modulos:
- services/ai.ts: cliente IA provider-agnostico (OpenRouter, MiniMax)
- services/db.ts: Dexie core con tabla settings + project_docs
- services/storage.ts: Cache-Aside + Write-Through (L1 Map → L2 Dexie → L3 localStorage)
- services/parse-transcription.ts: parser .docx/.vtt/.txt/.md
- services/session-analyzer.ts: extraccion IA de sesiones (resumen, tareas, decisiones)
- services/project-doc.ts: documento maestro MD (Bloque 1 resumen + Bloque 2 sesiones)
- stores/settings.ts: proveedores IA, modelos, API keys separadas por provider
- stores/transcriptions.ts: pipeline upload → analyze → create HU en KAPPA
- views/SettingsView.vue: configuracion IA (OpenRouter, MiniMax, OpenCode bridge)
- views/TranscriptionsView.vue: subida multiple + analisis sesion + visor MD + calendario
- components/AiProjectChat.vue: chat contextual por proyecto con selector de modelo

Cambios en existentes:
- stores/auth.ts, kappa-api.ts, upload-hu.ts: migrados a storage service (Dexie + localStorage)
- stores/projects.ts, workitems.ts: kappa_last_project via storage
- DashboardView.vue: descripcion reemplazada por AiProjectChat
- NewDashboardView.vue: tabs transcriptions + settings + navigate-settings events
- NavMain.vue: items Transcripciones + Configuracion
- SiteHeader.vue: labels tabs + language via storage
- LoginView.vue: remember_email via storage
- i18n: +80 keys español/ingles
- vite.config.ts: proxy CORS para MiniMax
- package.json: +mammoth.js
2026-05-28 12:42:30 -05:00

152 lines
5.3 KiB
TypeScript

/**
* 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<T>('k') → T | null (sync)
* await storage.setJSON() → void (async)
*/
import db from '@/services/db'
class AppStorage {
/** L1: cache en memoria */
private cache = new Map<string, { value: string; ttl: number | null }>()
private loaded = false
private initPromise: Promise<void> | 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<T>(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()