Persistence
Persistencia de Targ-Apps-Finance — Chroma-Fs (IndexedDB), snapshot síncrono en memoria y fallback a localStorage.
Targ-Apps-Finance corre enteramente en el cliente (Chroma + WASM, sin backend).
Los datos sobreviven al recargar porque src/main.js hidrata el snapshot
antes del primer render, y cada mutación se escribe de forma asíncrona a
Chroma-Fs.
Backend activo: Chroma-Fs
chroma.json declara "plugins": ["chroma-fs"]. La configuración vive en
chroma-fs.json y debe coincidir con CHROMA_FS_CONFIG en logic/persist.js:
{
"storage": {
"backend": "indexeddb",
"indexedDb": {
"dbName": "targ-apps-finance",
"storeName": "files"
}
}
}El snapshot canónico es el archivo virtual finance/snapshot.json dentro de esa
base IndexedDB.
| Campo | Valor |
|---|---|
| Plugin | chroma-fs (createStorage / openChromaStorage) |
| Backend navegador | indexeddb (CHROMA_FS_CONFIG / chroma-fs.json) |
| Backend Chroma Desktop | filesystem + postMessage (CHROMA_DESKTOP_FS_CONFIG en runtime) |
| DB / store (solo navegador) | targ-apps-finance / files |
| Path | finance/snapshot.json (SNAPSHOT_PATH) |
Web vs Chroma Desktop (Tauri)
chroma-fs.json declara backend: "indexeddb" para el navegador. En Chroma
Desktop la app corre en un <iframe> sin globals __TAURI__; IndexedDB ahí
es poco fiable (origen 127.0.0.1, partición cross-site). openChromaStorage()
detecta el iframe y sobrescribe en runtime a filesystem + puente
postMessage hacia el shell Tauri (archivos bajo el app-data del host).
| Entorno | Backend activo | getPersistBackend() | Dónde mirar los datos |
|---|---|---|---|
chroma dev (navegador) | IndexedDB | chroma-fs | DevTools → Application → IndexedDB → targ-apps-finance |
| Chroma Desktop (develop o zip) | Filesystem bridge | chroma-fs+filesystem | <app-data>/apps/<id>/data/finance/snapshot.json en disco |
resolveOpenStorageConfig() elige el backend; getPersistStorageMode() devuelve
indexeddb o filesystem. Activá trazas con globalThis.__TAF_PERSIST_DEBUG__ = true.
Verificación manual en desktop
cd Targ-Apps-Finance && npm run dev(condev.desktop: trueenchroma.json).- Chroma Desktop abre la app en develop mode.
- Creá una cuenta y cerrá/reabrí el iframe o Chroma Desktop.
- La cuenta debe seguir presente.
Consola del iframe (DevTools → inspeccionar el frame de la app):
getPersistBackend() // "chroma-fs+filesystem"
getPersistStorageMode() // "filesystem"
__tafPersist.resolveOpenStorageConfig()
await __chromaT2t.runAll()Si getPersistBackend() devuelve "memory" o "localStorage", Chroma-Fs no
está activo — revisá la consola por [TAF] Chroma-Fs open failed.
Hidratación (antes del render)
// src/main.js
import { hydratePersist, openChromaStorage } from "../logic/persist.js";
let chromaStorage = null;
try {
chromaStorage = await openChromaStorage();
} catch (err) {
console.warn("[TAF] Chroma-Fs preload failed:", err?.message ?? err);
}
const { backend } = await hydratePersist(chromaStorage ? { storage: chromaStorage } : {});
const { App } = await import("./App.js");
render(App, document.getElementById("app"));App se importa después de hidratar para que seedIfEmpty en
financeStore vea el snapshot real y no siembre categorías sobre un cache
vacío que luego se sobrescribe.
Orden de resolución en hydratePersist():
- Chroma-Fs —
openChromaStorage()conresolveOpenStorageConfig()(IndexedDB en navegador; filesystem bridge en iframe de Chroma Desktop).main.jsprecarga el storage y lo pasa ahydratePersist({ storage }). - Si no hay snapshot, migra claves legacy
taf:*delocalStorage. - Si Chroma-Fs no está disponible (tests, SSR, IDB bloqueado) →
localStorage
taf:*. - Si tampoco hay
localStorage→ memoria (la sesión no sobrevive al reload).
Errores al leer un snapshot corrupto no desactivan IndexedDB: se conserva el
storage y se escribe un snapshot nuevo en el próximo setPersisted.
flushPersist() propaga errores de escritura (ya no se tragan en silencio).
Un pagehide dispara flushPersist() para no perder writes pendientes.
logic/persist.js — cache síncrono + flush async
Los stores y repositorios siguen siendo síncronos. persist.js mantiene un
snapshot en memoria; getPersisted / setPersisted leen y escriben ese cache
y encolan un write a Chroma-Fs.
import {
hydratePersist,
flushPersist,
getPersisted,
setPersisted,
getPersistBackend,
getPersistStorageMode,
resolveOpenStorageConfig,
SNAPSHOT_PATH,
} from "../logic/persist.js";
await hydratePersist(); // una vez, en main.js
setPersisted("accounts", [...]); // sync + save async
await flushPersist(); // tests / exportForma del snapshot:
{
schemaVersion: 1,
transactions: [],
categories: [],
accounts: [],
budgets: [],
settings: { defaultCurrency, locale, weekStartsOn, theme } | null,
onboardingComplete: false
}onboardingComplete se pone en true al crear la primera cuenta (y se deriva
de accounts.length > 0 si el flag no venía en un snapshot viejo). Una vez
true, no se apaga aunque se borren todas las cuentas.
storage.js y repositorio
logic/persistence/storage.js es un adaptador fino sobre persist
(getItem/setItem/removeItem). repository.js no cambia: CRUD por
colección (transactions, categories, accounts, budgets).
Export/import (TAF-018) serializa el snapshot completo, incluido
onboardingComplete y settings. La lógica pura vive en logic/export.js y
logic/import.js; la UI usa logic/persistence/exportImport.js.
Tests
Ver Testing para la suite completa y el plugin
chroma-t2t.
test/logic/persist.test.js (mock createMemoryStorage(), fake-indexeddb, node --test) cubre:
chroma-fs.json≡CHROMA_FS_CONFIG- round-trip de cuentas, settings y flag de onboarding
- el flag sobrevive a
accounts: [] - fallback a memoria si IndexedDB no está disponible
- snapshot corrupto no bloquea escrituras posteriores
- round-trip real por
openChromaStorage+ IndexedDB simulado resolveOpenStorageConfig()(navegador vs iframe Chroma Desktop)- modo
chroma-fs+filesystemal hidratar en iframe - persistencia con globals Tauri en ventana top-level (sigue en IndexedDB)
flushPersistrechaza cuando el backend falla
test/logic/integration.test.js incluye un ciclo E2E onboarding → cuenta → reload
con restricciones tipo Tauri (IndexedDB + origen custom simulado).
Relación con el store
FinanceProvider y SettingsProvider no hablan con IndexedDB. Leen el cache
ya hidratado. Tras un import de backup, refreshFromStorage() vuelve a leer
los repositorios.