Saltar al contenido

Arquitectura de persistencia local y durabilidad del dato

Modelos de persistencia en disco: del almacenamiento virtualizado con desalojo en el navegador al sistema de archivos del host. Garantías ACID con SQLite embebido, durabilidad con fsync y formatos con esquema neutral.

El sandbox de almacenamiento web frente al sistema de archivos físico

Las aplicaciones web cuentan con mecanismos de persistencia local (IndexedDB, Cache API, Origin Private File System u OPFS). Sin embargo, estos mecanismos presentan limitaciones críticas impuestas por el modelo de seguridad del navegador:

  1. Gestión de cuotas y desalojo (Eviction): Los motores web aplican algoritmos LRU (Least Recently Used) para liberar espacio en disco. Si el almacenamiento global del dispositivo supera un umbral crítico, el navegador purga silenciosamente los datos de orígenes inactivos, incluso cuando la aplicación solicitó almacenamiento persistente mediante navigator.storage.persist().
  2. Confinamiento de origen: Los archivos guardados en OPFS están encapsulados en estructuras virtuales gestionadas por el motor del navegador. Ninguna herramienta externa (un script de shell, un gestor de respaldos como Time Machine o Restic, o una base de datos externa) puede inspeccionar, respaldar o reparar esos datos de forma independiente.
  3. Fragilidad ante la limpieza de navegación: Si el usuario borra las cookies o el historial del navegador, los datos de la aplicación se destruyen irremediablemente al compartir el mismo ciclo de vida de privacidad.
Texto
[Jerarquía de almacenamiento y durabilidad]Web (Sandbox del Navegador):  [JavaScript] ──> IndexedDB / OPFS ──> [Capa Virtual del Motor] ──> Cuota / Riesgo de Desalojo LRUEscritorio (Tauri + Rust):  [Webview UI] ──(IPC)──> [Proceso Core (Rust)] ──(syscalls)──> [Kernel VFS / Page Cache] ──(fsync)──> [Disco Físico]

En Tauri, la persistencia opera directamente sobre el subsistema de archivos virtual (Virtual File System o VFS) del kernel. El proceso Core en Rust resuelve la ruta canónica del sistema (app_data_dir), crea los archivos con descriptores nativos y delega la integridad a los mecanismos del sistema operativo.

Durabilidad física y la barrera de fsync

Escribir datos en el disco mediante llamadas estándar de alto nivel (std::fs::write o write() en POSIX) no garantiza que los bytes alcancen físicamente el medio magnético o de estado sólido (SSD):

Rust
use std::fs::File;use std::io::{self, Write};use std::path::Path;pub fn guardar_con_durabilidad(ruta: &Path, payload: &[u8]) -> io::Result<()> {    let mut archivo = File::create(ruta)?;    archivo.write_all(payload)?;    // La escritura anterior solo modificó la caché de páginas (Page Cache) en RAM del kernel.    // sync_all emite la syscall fsync() que fuerza al controlador del disco a vaciar    // sus buffers físicos antes de retornar éxito.    archivo.sync_all()?;    Ok(())}

Sin la llamada explícita a sync_all (fsync), los datos permanecen en la caché de páginas de la memoria RAM. Si se produce un fallo de alimentación eléctrica o un kernel panic segundos después, los datos se pierden o el archivo se corrompe. En el escritorio, el control de la durabilidad pertenece al ingeniero: se decide cuándo asumir la sobrecarga de I/O de sincronizar físicamente contra el disco para garantizar consistencia transaccional.

SQLite como formato de archivo universal y neutral

Al estructurar la persistencia de una aplicación de escritorio, la elección del formato de archivo define la longevidad y la interoperabilidad del sistema:

El patrón arquitectónico recomendado para aplicaciones complejas en Tauri es integrar SQLite embebido en el proceso Core (mediante crates como rusqlite o sqlx):

Rust
// Inicialización de SQLite embebido en el Core de Tauriuse rusqlite::{params, Connection, Result};use std::path::Path;pub fn inicializar_almacen_local(ruta_db: &Path) -> Result<Connection> {    let conn = Connection::open(ruta_db)?;    // Habilita el modo Write-Ahead Logging (WAL) para lecturas concurrentes sin bloqueo    conn.pragma_update(None, "journal_mode", "WAL")?;    conn.pragma_update(None, "synchronous", "NORMAL")?;    conn.execute(        "CREATE TABLE IF NOT EXISTS documentos (            id TEXT PRIMARY KEY,            titulo TEXT NOT NULL,            cuerpo TEXT NOT NULL,            actualizado_en INTEGER NOT NULL        )",        [],    )?;    Ok(conn)}

Este enfoque ofrece tres ventajas estructurales:

  1. Concurrencia segura: El modo WAL permite que los hilos de lectura de la interfaz consulten datos mientras un hilo de fondo escribe, sin contención de cerrojos.
  2. Durabilidad ante fallos: SQLite recupera el estado consistente en el arranque si el proceso fue terminado abruptamente (SIGKILL).
  3. Soberanía y portabilidad: El archivo generado es independiente del sistema operativo, la arquitectura de CPU (little-endian/big-endian) y el ciclo de vida del framework.

Frontera conceptual: Tauri frente a Local-First

Es fundamental distinguir la capa de plataforma de la capa de arquitectura de datos:

  • Tauri es la concha de plataforma: Proporciona el entorno de ejecución nativo, acceso a llamadas de sistema de archivos, descriptores de bajo nivel y ejecución de hilos en Rust sin restricciones de red o CORS.
  • Local-First es la disciplina de datos: Define cómo se estructuran las entidades para permitir mutaciones locales sin red, converger réplicas distribuidas y resolver conflictos mediante estructuras matemáticas (CRDTs, State-based o Operation-based).

Una aplicación Tauri puede diseñarse como un cliente dependiente de una API centralizada, o como un nodo completamente soberano y Local-First. Tauri no impone la arquitectura, pero proporciona las primitivas de almacenamiento de alta fidelidad que una arquitectura Local-First rigurosa exige.