114 / Advanced

Offline IndexedDB

std/browser_idb brings IndexedDB with await: idb_get/idb_put/idb_delete/idb_keys are asynchronous, return Result<_, Error> with the usual kinds, and are built for what outgrows the ~5 MB limit and synchronous writes of localStorage: a catalog of thousands of records and a queue of pending operations in an offline PWA. A single database with a composite [store, key] key, so a new store never triggers a version bump that would block across tabs.

114-idb-offline-wasm.nxSource →
// 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))
}
Illustrative outputstdout
ventas drenadas: 1

How it works

guardar_catalogo/cargar_catalogo use the "catalogo" store; encolar_venta/drenar_cola use "cola-ventas", a SEPARATE logical namespace: the two never mix in idb_keys() even though they share a single physical database. idb_get on a missing key is Err(kind: "not_found"), not an empty string — the same closed Error vocabulary as the rest of the stdlib, so cargar_catalogo tells "first visit" apart from a real failure.

idb_put/idb_delete return Result<int, Error>, and delete is idempotent: Ok even if the key is already gone, so draining the queue twice does not blow up. drenar_cola walks idb_keys("cola-ventas") and processes each queued sale — here deleting it afterward, in a real case sending the matching POST before the idb_delete.

Same "one suspended stack at a time" contract as std/browser_await: if another part of the screen fires an idb_* while this await is waiting, it gets queued and processed in order.