132 / Advanced

A PWA talking to a local-network server

The tablets of a shop have the application installed (a PWA) and talk to a server on the same network, so they keep working when the internet goes down. This is a browser recipe for wasm32-wasi: it is built with nyx build --target wasm32-wasi and uses the DOM, so it cannot run natively and the output above is illustrative.

132-browser-fetch-red-local.nxSource →
// 132 — Una PWA que habla con un servidor de la red local, con plazo
//
// Target wasm32-wasi (corre en el navegador, o bajo node con
// examples/browser/run-node.mjs). El caso real: las tablets de un local
// tienen la aplicación instalada (PWA) y hablan con un servidor que está en la
// misma red, para seguir funcionando aunque se caiga internet.
//
// 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:
//   - Chrome pide el permiso de red local (Local Network Access, antes Private
//     Network Access) para que una página hable con un equipo de la LAN. Se
//     activa con `targetAddressSpace` en fetch: el tercer argumento
//     "local" de estas funciones. "" = no pedirlo; un navegador que no conoce
//     el campo lo ignora, así que nunca rompe el pedido.
//   - El servidor también tiene que cooperar (CORS y, si el navegador manda
//     un preflight de red local, contestarlo). Esto NO se pudo verificar
//     contra un Chrome real desde el repo: los tests fijan que el shim pasa
//     el campo y que el plazo aborta el pedido.
//   - Contra un equipo apagado el fetch espera minutos: el plazo (en ms) lo
//     aborta y devuelve Err con kind "timeout" (o "connection" si no hubo
//     respuesta alguna).
//   - En SSE el plazo es para CONECTAR, no para la vida del canal. Si vence
//     (o la conexión falla) el manejador recibe UNA vez el evento
//     "nyx-error" con dato "timeout" o "connection", y el canal se cierra solo.

import "std/browser"
import "std/browser_await"
import "std/dom"
import "std/error"

const SERVIDOR: String = "http://192.168.1.20:8080"

// Manda un pedido al servidor local: si no responde en 3 s, avisa en vez de
// dejar la tablet esperando.
async fn enviar_pedido(cuerpo: String) -> String {
    let res = await browser_fetch_await_net(SERVIDOR + "/pedidos", "POST", cuerpo, 3000, "local")
    match res {
        Result.Ok(r) => { return "guardado (HTTP " + int_to_string(r.status) + ")" }
        Result.Err(e) => {
            if e.kind == "timeout" { return "el servidor local no responde: se guarda para más tarde" }
            return "sin conexión con el servidor local: " + e.msg
        }
    }
}

#[export_name = "enviar"]
fn enviar() {
    dom_set_text("#estado", "enviando...")
    let texto: String = await enviar_pedido("{\"mesa\":4,\"items\":2}")
    dom_set_text("#estado", texto)
}

fn main() {
    // Aviso de cambios del servidor local; el mismo plazo y permiso.
    let canal: int = browser_sse_opts_fn(SERVIDOR + "/eventos", 3000, "local",
        fn(evento: String, datos: String) {
            if evento == "nyx-error" {
                dom_set_text("#red", "sin servidor local (" + datos + ")")
            } else {
                dom_set_text("#red", evento + ": " + datos)
            }
        })
    if canal == 0 { dom_set_text("#red", "el navegador no permite SSE") }
    // Con cierre, en vez de await: la misma opción para un fetch de un disparo.
    browser_fetch_opts_fn(SERVIDOR + "/estado", "GET", "", 3000, "local",
        fn(status: int, body: String) {
            if status == 0 { dom_set_text("#estado", body) } else { dom_set_text("#estado", "servidor: " + body) }
        })
}
Illustrative outputstdout
#estado: enviando...
#estado: el servidor local no responde: se guarda para más tarde
#red: sin servidor local (timeout)

How it works

Chrome asks for the Local Network Access permission (formerly Private Network Access) before a page may talk to a machine on the LAN. It is requested with targetAddressSpace on fetch — the third argument "local" of these functions; "" means do not ask, and a browser that does not know the field ignores it, so it never breaks the request. The server has to cooperate too: CORS and, if the browser sends a local-network preflight, an answer to it. That part could not be verified against a real Chrome from the repository; the tests pin that the shim passes the field and that the timeout aborts the request.

Against a machine that is off, a fetch waits for minutes. The timeout (in ms, 3000 here) aborts it and returns an Err with kind "timeout", or "connection" if there was no response at all, which enviar_pedido turns into a message instead of leaving the tablet hanging. On SSE the timeout is for CONNECTING, not for the life of the channel: if it expires, or the connection fails, the handler receives the "nyx-error" event ONCE with data "timeout" or "connection", and the channel closes by itself. Because the program waits on the host, nyx build needs binaryen (or NYX_WASM_OPT).