package httputil import ( "bytes" "encoding/gob" "encoding/json" ) // Client-side fetch helpers — the counterparts to RespondGob / RespondJSON. In a // gowasm app the server and the WebAssembly client share the same Go types, so // the server encodes with RespondGob(data) and the client decodes straight back // into that type with FetchGob — no JSON, no hand-written unmarshalling. These // are neutral (they only touch encoding/*), so app code can call them while // staying SSR-able; the actual HTTP transport is client-only and installed at // startup by the wasm runtime (SetClientTransport), the inversion vdom.Schedule // uses. // clientTransport does an HTTP GET returning the raw body. The wasm runtime // installs it in its init, so it is always set in the browser and always nil // everywhere else — "nil transport" therefore means "not the client", which is // what makes the SSR behavior in fetchInto safe. var clientTransport func(url string) ([]byte, error) // SetClientTransport installs the HTTP GET used by FetchGob / FetchJSON. The // wasm client runtime installs wasmruntime.FetchBytes automatically; call this // only to wrap it (auth headers, a base URL, a test double). func SetClientTransport(get func(url string) ([]byte, error)) { clientTransport = get } func fetchInto[T any](url string, cb func(T, error), decode func([]byte, *T) error) { // Off the client (SSR of a static page, a native build) there is no transport // and no browser to fetch with, so the request simply never happens: cb is not // called, and the caller stays in the loading state it started in. That is // what SSR should ship — a spinner or skeleton, not a "fetch failed" error the // server was never able to avoid. The client re-runs the fetch for real on its // first render after hydration. if clientTransport == nil { return } go func() { var out T body, err := clientTransport(url) if err != nil { cb(out, err) return } if err := decode(body, &out); err != nil { cb(out, err) return } cb(out, nil) }() } // FetchGob GETs url and gob-decodes the body into T (pair with RespondGob), then // calls cb(result, err) on a background goroutine — safe to call from a render // or event handler; set the result into a signal in the callback. T is inferred // from the callback: // // httputil.FetchGob("/api/quotes", func(qs []Quote, err error) { // if err != nil { /* … */ } else { quotes.Set(qs) } // }) // // Fetching is client-only: during SSR this does nothing and cb is never called, // so a page that starts out loading renders its spinner into the static HTML and // fetches for real once the client takes over. func FetchGob[T any](url string, cb func(T, error)) { fetchInto(url, cb, func(b []byte, out *T) error { return gob.NewDecoder(bytes.NewReader(b)).Decode(out) }) } // FetchJSON GETs url and json-decodes the body into T — for third-party JSON // APIs (or your own RespondJSON). Same callback/goroutine shape as FetchGob. func FetchJSON[T any](url string, cb func(T, error)) { fetchInto(url, cb, func(b []byte, out *T) error { return json.Unmarshal(b, out) }) }