114 / Avanzado

IndexedDB sin conexión

std/browser_idb trae IndexedDB con await: idb_get/idb_put/idb_delete/idb_keys son asíncronos, devuelven Result<_, Error> con los kind de siempre, y pensados para lo que se sale del límite de ~5 MB y la escritura síncrona de localStorage: un catálogo de miles de registros y una cola de operaciones pendientes en una PWA sin conexión. Una sola base con clave compuesta [store, key], así que un store nuevo no dispara una subida de versión que bloquee entre pestañas.

114-idb-offline-wasm.nxFuente →
// 114 — IndexedDB con await: un catálogo que sobrevive a un F5, sin callbacks
//
// Target wasm32-wasi (corre en el navegador, o bajo node con
// examples/browser/run-node.mjs). Pensado para una PWA sin conexión: un
// catálogo de miles de registros y una cola de operaciones pendientes, algo
// que se sale del límite de ~5 MB y la escritura síncrona de localStorage
// (ls_get/ls_set, std/browser).
//
// Construir:  nyx build --target wasm32-wasi
//   (necesita binaryen: `sudo apt install binaryen`, o NYX_WASM_OPT=/ruta/a/wasm-opt;
//    nyx build lo usa SOLO porque el programa espera al anfitrión)
//
// Lo que conviene saber:
//   - idb_get de una clave ausente es Err(kind: "not_found"), no "" — el
//     mismo vocabulario cerrado de Error que el resto de la stdlib.
//   - `store` ("catalogo", "cola-ventas" acá abajo) es un namespace lógico:
//     dos stores nunca se mezclan aunque compartan una sola base física —
//     ver std/browser_idb.nx para el porqué (clave compuesta [store, key]).
//   - idb_put/idb_delete son Result<int, Error>: Ok(0) en éxito, y delete es
//     idempotente (Ok aunque la clave ya no exista).
//   - Mismo contrato de "una sola pila suspendida" que browser_await: si otra
//     parte de la pantalla dispara un idb_* mientras este espera, se encola.

import "std/browser_idb"
import "std/error"

// Guarda el catálogo completo como JSON. Devuelve true si quedó persistido.
async fn guardar_catalogo(json: String) -> bool {
    let r = await idb_put("catalogo", "items", json)
    match r {
        Result.Ok(_) => { return true }
        Result.Err(e) => { println("no se pudo guardar el catálogo: " + e.kind); return false }
    }
}

// Lee el catálogo guardado, o "" si todavía no hay uno (primera visita).
async fn cargar_catalogo() -> String {
    let r = await idb_get("catalogo", "items")
    match r {
        Result.Ok(json) => { return json }
        Result.Err(e) => {
            if e.kind == "not_found" { return "" }
            println("no se pudo leer el catálogo: " + e.kind)
            return ""
        }
    }
}

// Encola una venta hecha sin conexión, bajo su propio ID, en un store
// APARTE del catálogo — nunca se van a mezclar en idb_keys().
async fn encolar_venta(id: String, venta_json: String) -> bool {
    let r = await idb_put("cola-ventas", id, venta_json)
    match r {
        Result.Ok(_) => { return true }
        Result.Err(e) => { println("no se pudo encolar la venta " + id + ": " + e.kind); return false }
    }
}

// Cuando vuelve la conexión: procesa y borra cada venta encolada.
async fn drenar_cola() -> int {
    let claves = await idb_keys("cola-ventas")
    match claves {
        Result.Ok(ids) => {
            let ventas: Array<String> = ids
            var i: int = 0
            while i < ventas.length() {
                let id: String = ventas[i]
                // acá iría el POST real al servidor con el JSON de la venta
                let _b = await idb_delete("cola-ventas", id)
                i = i + 1
            }
            return ventas.length()
        }
        Result.Err(e) => { println("no se pudo leer la cola: " + e.kind); return 0 }
    }
}

fn main() {
    let _g: bool = await guardar_catalogo("{\"items\":[{\"id\":1,\"nombre\":\"tornillo\"}]}")
    let _v: bool = await encolar_venta("venta-0042", "{\"total\":120}")
    let procesadas: int = await drenar_cola()
    println("ventas drenadas: " + int_to_string(procesadas))
}
Salida ilustrativastdout
ventas drenadas: 1

Cómo funciona

guardar_catalogo/cargar_catalogo usan el store "catalogo"; encolar_venta/drenar_cola usan "cola-ventas", un namespace lógico APARTE: los dos nunca se mezclan en idb_keys() aunque compartan una sola base física. idb_get de una clave ausente es Err(kind: "not_found"), no una cadena vacía — el mismo vocabulario cerrado de Error que el resto de la stdlib, así que cargar_catalogo distingue «primera visita» de un fallo real.

idb_put/idb_delete devuelven Result<int, Error>, y delete es idempotente: Ok aunque la clave ya no exista, así que drenar la cola dos veces no revienta. drenar_cola recorre idb_keys("cola-ventas") y procesa cada venta encolada —acá borrándola después, en un caso real mandando el POST correspondiente antes del idb_delete.

Mismo contrato de «una sola pila suspendida a la vez» que std/browser_await: si otra parte de la pantalla dispara un idb_* mientras este await espera, se encola y se procesa en orden.