gob fetcher for wasm example
This commit is contained in:
@@ -16,10 +16,19 @@ go run ./server # codegen + SSR + hot reload at http://localhost:8085
|
|||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:8085. `/` and `/about` use the light **public** layout;
|
Open http://localhost:8085. `/` and `/about` use the light **public** layout;
|
||||||
`/chart`, `/server`, and **`/kit`** (a UI-kit "kitchen-sink" demo of the webui
|
`/chart`, `/server`, **`/data`** (client-side fetching), and **`/kit`** (a UI-kit
|
||||||
components) use the dark **app** layout. Edit any `.go` file and the browser
|
"kitchen-sink" demo of the webui components) use the dark **app** layout. Edit
|
||||||
hot-swaps the new wasm **without a full reload or a flash**, preserving page
|
any `.go` file and the browser hot-swaps the new wasm **without a full reload or
|
||||||
state; a build failure shows the Go compiler output as an overlay.
|
a flash**, preserving page state; a build failure shows the Go compiler output
|
||||||
|
as an overlay.
|
||||||
|
|
||||||
|
**Data fetching** (`/data`) shows both directions of `kjol/httputil`: the server
|
||||||
|
answers `/api/quotes` with `httputil.RespondGob([]Quote)` and the client decodes
|
||||||
|
it straight back into `[]Quote` with `httputil.FetchGob` (the same Go type on
|
||||||
|
both ends — no JSON); and a **user-entered** GitHub repo (`owner/name`) is
|
||||||
|
fetched with `httputil.FetchJSON` into a tagged Go struct. The client HTTP
|
||||||
|
transport is `wasmruntime.FetchBytes`, injected once via
|
||||||
|
`httputil.SetClientTransport`.
|
||||||
|
|
||||||
Styling is **Tailwind**: the dev server (and `build.sh`) run `kjol/cmd/twcss`,
|
Styling is **Tailwind**: the dev server (and `build.sh`) run `kjol/cmd/twcss`,
|
||||||
which scans the Go markup + the `webui` kit for utility classes and compiles
|
which scans the Go markup + the `webui` kit for utility classes and compiles
|
||||||
@@ -41,6 +50,7 @@ app/ the application — neutral, standalone functions (no centra
|
|||||||
pages.go Deps + Shell + App/Public layouts + nav + Counter + pages
|
pages.go Deps + Shell + App/Public layouts + nav + Counter + pages
|
||||||
chart.go Chart page (go-chart, renders on both sides)
|
chart.go Chart page (go-chart, renders on both sides)
|
||||||
kit.go /kit — UI-kit demo page showcasing kjol/webui components
|
kit.go /kit — UI-kit demo page showcasing kjol/webui components
|
||||||
|
data.go /data — client fetch: gob from /api/quotes + third-party JSON
|
||||||
server_counter.go //gowasm:server component (server-only; clicks-over-time chart)
|
server_counter.go //gowasm:server component (server-only; clicks-over-time chart)
|
||||||
*.gen.go GENERATED by kjol/cmd/wasmgen (routes, layout dispatch, stubs)
|
*.gen.go GENERATED by kjol/cmd/wasmgen (routes, layout dispatch, stubs)
|
||||||
css/app.css Tailwind entry (@import "tailwindcss" + @theme tokens)
|
css/app.css Tailwind entry (@import "tailwindcss" + @theme tokens)
|
||||||
@@ -57,6 +67,7 @@ wwwroot/ wasmboot.js (+ generated app.css, wasm_exec.js, app.wasm)
|
|||||||
| `kjol/wasmruntime` | wasm client runtime: reconcile, `Run`/`Hydrate`, router, fetch, HMR state | `wasm/main.go` calls `Hydrate`/`Run` |
|
| `kjol/wasmruntime` | wasm client runtime: reconcile, `Run`/`Hydrate`, router, fetch, HMR state | `wasm/main.go` calls `Hydrate`/`Run` |
|
||||||
| `kjol/rsc` | stateless server components over HTTP (gob) | `//gowasm:server` + the generated client stub |
|
| `kjol/rsc` | stateless server components over HTTP (gob) | `//gowasm:server` + the generated client stub |
|
||||||
| `kjol/wasmdevserver` | reusable dev server: SSR, `/rsc`, hot reload, error overlay | `server/main.go` fills a `wasmdevserver.Config` |
|
| `kjol/wasmdevserver` | reusable dev server: SSR, `/rsc`, hot reload, error overlay | `server/main.go` fills a `wasmdevserver.Config` |
|
||||||
|
| `kjol/httputil` | gob/JSON responders + typed client fetch (`RespondGob`, `FetchGob`, `FetchJSON`) | the `/data` page + the `/api/quotes` handler |
|
||||||
| `kjol/cmd/wasmgen` | directive codegen → `app/*.gen.go` | run by `buildWasm` and `//go:generate` |
|
| `kjol/cmd/wasmgen` | directive codegen → `app/*.gen.go` | run by `buildWasm` and `//go:generate` |
|
||||||
|
|
||||||
The **golden rule** holds: `wasmdevserver` imports no app code. The app injects
|
The **golden rule** holds: `wasmdevserver` imports no app code. The app injects
|
||||||
|
|||||||
147
go/cmd/examples/go-wasm-web/app/data.go
Normal file
147
go/cmd/examples/go-wasm-web/app/data.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kjol/httputil"
|
||||||
|
. "kjol/vdom"
|
||||||
|
ui "kjol/webui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Quote is the payload the /api/quotes endpoint returns. The server encodes a
|
||||||
|
// []Quote with httputil.RespondGob; the client decodes it straight back into
|
||||||
|
// []Quote — the SAME Go type, no JSON, no hand-written unmarshalling.
|
||||||
|
type Quote struct {
|
||||||
|
Author string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoInfo is a subset of GitHub's repo JSON (a third-party API), tagged for
|
||||||
|
// json decoding.
|
||||||
|
type repoInfo struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Stars int `json:"stargazers_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//gowasm:page /data layout=app
|
||||||
|
func DataPage(d Deps) func() *VNode {
|
||||||
|
// (1) gob from our own server via httputil.RespondGob / FetchGob.
|
||||||
|
quotes := NewSignal([]Quote{})
|
||||||
|
qLoading := NewSignal(true)
|
||||||
|
qErr := NewSignal("")
|
||||||
|
// (2) JSON from a third-party API (GitHub), for a user-entered repo.
|
||||||
|
repo := NewSignal(repoInfo{})
|
||||||
|
rLoading := NewSignal(true)
|
||||||
|
rErr := NewSignal("")
|
||||||
|
repoQuery := NewSignal("golang/go")
|
||||||
|
started := false
|
||||||
|
|
||||||
|
// fetchRepo loads owner/name from the GitHub API into the repo signal.
|
||||||
|
fetchRepo := func(q string) {
|
||||||
|
q = strings.Trim(strings.TrimSpace(q), "/")
|
||||||
|
if q == "" {
|
||||||
|
rErr.Set("enter a repo as owner/name")
|
||||||
|
rLoading.Set(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rErr.Set("")
|
||||||
|
rLoading.Set(true)
|
||||||
|
httputil.FetchJSON("https://api.github.com/repos/"+q, func(r repoInfo, err error) {
|
||||||
|
if err != nil {
|
||||||
|
rErr.Set(err.Error())
|
||||||
|
} else {
|
||||||
|
repo.Set(r)
|
||||||
|
}
|
||||||
|
rLoading.Set(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() *VNode {
|
||||||
|
// Fire the initial fetches once, on the client (no transport on the server,
|
||||||
|
// so SSR ships the loading state and the client takes over).
|
||||||
|
if !started {
|
||||||
|
started = true
|
||||||
|
httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
|
||||||
|
if err != nil {
|
||||||
|
qErr.Set(err.Error())
|
||||||
|
} else {
|
||||||
|
quotes.Set(qs)
|
||||||
|
}
|
||||||
|
qLoading.Set(false)
|
||||||
|
})
|
||||||
|
fetchRepo(repoQuery.Get())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Div(Attr("class", "space-y-8"),
|
||||||
|
Div(
|
||||||
|
H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("Data fetching")),
|
||||||
|
P(Attr("class", "mt-1 text-neutral-500"), Text("Two client-side fetches: gob from our own server, and JSON from a third-party API you choose.")),
|
||||||
|
),
|
||||||
|
|
||||||
|
ui.Card("",
|
||||||
|
ui.CardHeader("", Text("gob — from our server")),
|
||||||
|
P(Attr("class", "mb-3 text-sm text-neutral-500"),
|
||||||
|
Text("The client GETs /api/quotes; the server responds with httputil.RespondGob "+
|
||||||
|
"(a gob-encoded []Quote) and httputil.FetchGob decodes it straight into []Quote — "+
|
||||||
|
"the same Go type on both ends, no JSON.")),
|
||||||
|
quotesBody(qLoading.Get(), qErr.Get(), quotes.Get()),
|
||||||
|
),
|
||||||
|
|
||||||
|
ui.Card("",
|
||||||
|
ui.CardHeader("", Text("JSON — from a third-party API")),
|
||||||
|
P(Attr("class", "mb-3 text-sm text-neutral-500"),
|
||||||
|
Text("Enter a GitHub repo; the client GETs api.github.com and httputil.FetchJSON "+
|
||||||
|
"decodes the response into a Go struct with `json:\"…\"` tags.")),
|
||||||
|
row("mb-4 flex items-end gap-2",
|
||||||
|
row("flex grow flex-col gap-1 max-w-sm",
|
||||||
|
ui.FormLabel(ui.FormLabelProps{}, Text("GitHub repo (owner/name)")),
|
||||||
|
ui.FormInput(ui.FormInputProps{
|
||||||
|
Value: repoQuery.Get(),
|
||||||
|
Placeholder: "golang/go",
|
||||||
|
OnInput: func(v string) { repoQuery.Set(v) },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Fetch", OnClick: func() { fetchRepo(repoQuery.Get()) }}),
|
||||||
|
),
|
||||||
|
repoBody(rLoading.Get(), rErr.Get(), repo.Get()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func quotesBody(loading bool, failed string, quotes []Quote) *VNode {
|
||||||
|
switch {
|
||||||
|
case failed != "":
|
||||||
|
return ui.Alert(ui.AlertRed, "Fetch failed", Text(failed))
|
||||||
|
case loading:
|
||||||
|
return ui.Loader()
|
||||||
|
default:
|
||||||
|
cards := make([]*VNode, 0, len(quotes))
|
||||||
|
for _, q := range quotes {
|
||||||
|
cards = append(cards, ui.BorderCard("",
|
||||||
|
P(Attr("class", "text-neutral-800"), Text("“"+q.Text+"”")),
|
||||||
|
P(Attr("class", "mt-2 text-sm text-neutral-500"), Text("— "+q.Author)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return row("grid gap-3 sm:grid-cols-2", cards...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func repoBody(loading bool, failed string, r repoInfo) *VNode {
|
||||||
|
switch {
|
||||||
|
case failed != "":
|
||||||
|
return ui.Alert(ui.AlertRed, "Fetch failed", Text(failed))
|
||||||
|
case loading:
|
||||||
|
return ui.Loader()
|
||||||
|
default:
|
||||||
|
return ui.BorderCard("",
|
||||||
|
row("flex items-center gap-2",
|
||||||
|
Strong(Attr("class", "text-neutral-800"), Text(r.FullName)),
|
||||||
|
ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber}, Text("★ "+strconv.Itoa(r.Stars))),
|
||||||
|
),
|
||||||
|
P(Attr("class", "mt-2 text-sm text-neutral-600"), Text(r.Description)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ func AppLayout(d Deps, content *VNode) *VNode {
|
|||||||
Ul(Attr("class", "ml-4 flex items-center gap-1"),
|
Ul(Attr("class", "ml-4 flex items-center gap-1"),
|
||||||
navItem(d, "/chart", "Chart", true),
|
navItem(d, "/chart", "Chart", true),
|
||||||
navItem(d, "/server", "Server", true),
|
navItem(d, "/server", "Server", true),
|
||||||
|
navItem(d, "/data", "Data", true),
|
||||||
navItem(d, "/kit", "UI Kit", true)),
|
navItem(d, "/kit", "UI Kit", true)),
|
||||||
Ul(Attr("class", "ml-auto flex items-center"),
|
Ul(Attr("class", "ml-auto flex items-center"),
|
||||||
navItem(d, "/", "Home", true)),
|
navItem(d, "/", "Home", true)),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ func Routes(d Deps) map[string]func() *vdom.VNode {
|
|||||||
"/": HomePage(d),
|
"/": HomePage(d),
|
||||||
"/about": AboutPage(d),
|
"/about": AboutPage(d),
|
||||||
"/chart": ChartPage(d),
|
"/chart": ChartPage(d),
|
||||||
|
"/data": DataPage(d),
|
||||||
"/kit": KitPage(d),
|
"/kit": KitPage(d),
|
||||||
"/server": ServerPage(d),
|
"/server": ServerPage(d),
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ var RouteLayout = map[string]string{
|
|||||||
"/": "public",
|
"/": "public",
|
||||||
"/about": "public",
|
"/about": "public",
|
||||||
"/chart": "app",
|
"/chart": "app",
|
||||||
|
"/data": "app",
|
||||||
"/kit": "app",
|
"/kit": "app",
|
||||||
"/server": "app",
|
"/server": "app",
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -12,10 +12,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"kjol/httputil"
|
||||||
"kjol/vdom"
|
"kjol/vdom"
|
||||||
"kjol/wasmdevserver"
|
"kjol/wasmdevserver"
|
||||||
|
|
||||||
@@ -35,9 +37,28 @@ func main() {
|
|||||||
Build: buildWasm,
|
Build: buildWasm,
|
||||||
Render: render,
|
Render: render,
|
||||||
Document: document,
|
Document: document,
|
||||||
|
Handle: apiRoutes,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apiRoutes registers the example's API endpoints. /api/quotes responds with a
|
||||||
|
// gob-encoded []app.Quote (via httputil.RespondGob) — the /data page fetches and
|
||||||
|
// decodes it on the client with encoding/gob (Go types end to end, no JSON).
|
||||||
|
func apiRoutes(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("GET /api/quotes", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
httputil.RespondGob(w, http.StatusOK, sampleQuotes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleQuotes() []app.Quote {
|
||||||
|
return []app.Quote{
|
||||||
|
{Author: "Rob Pike", Text: "A little copying is better than a little dependency."},
|
||||||
|
{Author: "Rob Pike", Text: "Don't communicate by sharing memory; share memory by communicating."},
|
||||||
|
{Author: "Ken Thompson", Text: "When in doubt, use brute force."},
|
||||||
|
{Author: "Alan Kay", Text: "The best way to predict the future is to invent it."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// render SSRs a static route's #app inner HTML; ok=false ships an empty #app
|
// render SSRs a static route's #app inner HTML; ok=false ships an empty #app
|
||||||
// (client-rendered). It's the same neutral render the client runs, so the client
|
// (client-rendered). It's the same neutral render the client runs, so the client
|
||||||
// hydrates it.
|
// hydrates it.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gowasmweb/app"
|
"gowasmweb/app"
|
||||||
|
"kjol/httputil"
|
||||||
"kjol/vdom"
|
"kjol/vdom"
|
||||||
"kjol/wasmruntime"
|
"kjol/wasmruntime"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,7 @@ func main() {
|
|||||||
// across an in-place hot swap. On a normal load RestoreState is nil (fresh
|
// across an in-place hot swap. On a normal load RestoreState is nil (fresh
|
||||||
// state); after a dev hot-reload it carries the previous instance's values,
|
// state); after a dev hot-reload it carries the previous instance's values,
|
||||||
// which NewSignal restores by creation order.
|
// which NewSignal restores by creation order.
|
||||||
|
httputil.SetClientTransport(wasmruntime.FetchBytes) // typed gob/json fetches for app pages
|
||||||
collector := vdom.BeginCollect(wasmruntime.RestoreState())
|
collector := vdom.BeginCollect(wasmruntime.RestoreState())
|
||||||
router := wasmruntime.NewRouter()
|
router := wasmruntime.NewRouter()
|
||||||
deps := app.Deps{Path: router.Path, Navigate: router.Navigate}
|
deps := app.Deps{Path: router.Path, Navigate: router.Navigate}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
66
go/httputil/fetch.go
Normal file
66
go/httputil/fetch.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 injected at
|
||||||
|
// startup by the wasm runtime (SetClientTransport), the inversion vdom.Schedule
|
||||||
|
// uses.
|
||||||
|
|
||||||
|
// clientTransport does an HTTP GET returning the raw body; nil on the server.
|
||||||
|
var clientTransport func(url string) ([]byte, error)
|
||||||
|
|
||||||
|
// SetClientTransport installs the HTTP GET used by FetchGob / FetchJSON. The
|
||||||
|
// wasm client runtime calls this at startup with wasmruntime.FetchBytes.
|
||||||
|
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) {
|
||||||
|
go func() {
|
||||||
|
var out T
|
||||||
|
if clientTransport == nil {
|
||||||
|
cb(out, errors.New("httputil: no client transport installed (client-only)"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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) }
|
||||||
|
// })
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package httputil
|
package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/gob"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -11,6 +12,12 @@ func RespondJSON(w http.ResponseWriter, statusCode int, data any) {
|
|||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RespondGob(w http.ResponseWriter, statusCode int, data any) {
|
||||||
|
w.Header().Set("Content-Type", "application/gob")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
gob.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
func RespondError(w http.ResponseWriter, statusCode int, message string) {
|
func RespondError(w http.ResponseWriter, statusCode int, message string) {
|
||||||
RespondJSON(w, statusCode, map[string]string{"error": message})
|
RespondJSON(w, statusCode, map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type Config struct {
|
|||||||
Build func() ([]byte, error) // (re)build the wasm bundle; combined output on failure
|
Build func() ([]byte, error) // (re)build the wasm bundle; combined output on failure
|
||||||
Render func(path string) (inner string, ok bool) // SSR the #app inner HTML for a route (ok=false => client-rendered)
|
Render func(path string) (inner string, ok bool) // SSR the #app inner HTML for a route (ok=false => client-rendered)
|
||||||
Document func(inner string) string // wrap #app inner HTML in a full HTML document
|
Document func(inner string) string // wrap #app inner HTML in a full HTML document
|
||||||
|
Handle func(mux *http.ServeMux) // optional: register extra routes (e.g. app API endpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve builds once (in watch mode), wires the routes, and blocks serving.
|
// Serve builds once (in watch mode), wires the routes, and blocks serving.
|
||||||
@@ -70,6 +71,9 @@ func Serve(cfg Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux.HandleFunc("POST /rsc", rsc.Handler) // server components
|
mux.HandleFunc("POST /rsc", rsc.Handler) // server components
|
||||||
|
if cfg.Handle != nil {
|
||||||
|
cfg.Handle(mux) // app-registered routes (API endpoints, etc.)
|
||||||
|
}
|
||||||
mux.HandleFunc("/", rootHandler(cfg))
|
mux.HandleFunc("/", rootHandler(cfg))
|
||||||
|
|
||||||
log.Printf("serving %q on http://localhost%s", cfg.Dir, cfg.Addr)
|
log.Printf("serving %q on http://localhost%s", cfg.Dir, cfg.Addr)
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ package wasmruntime
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// absURL resolves a relative path against the current origin (a browser resolves
|
||||||
|
// relative fetch URLs itself, but building the absolute URL keeps behavior
|
||||||
|
// uniform — and lets non-browser hosts, e.g. tests, fetch too).
|
||||||
|
func absURL(url string) string {
|
||||||
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
loc := js.Global().Get("location")
|
||||||
|
return loc.Get("protocol").String() + "//" + loc.Get("host").String() + url
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch performs an HTTP GET via the browser Fetch API and returns the response
|
// Fetch performs an HTTP GET via the browser Fetch API and returns the response
|
||||||
// body as a string. It bridges a JS Promise into Go: it registers then/catch
|
// body as a string. It bridges a JS Promise into Go: it registers then/catch
|
||||||
// callbacks and blocks the calling goroutine on a channel until the request
|
// callbacks and blocks the calling goroutine on a channel until the request
|
||||||
@@ -67,7 +79,7 @@ func Fetch(url string) (string, error) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
js.Global().Call("fetch", url).Call("then", onResp).Call("catch", onErr)
|
js.Global().Call("fetch", absURL(url)).Call("then", onResp).Call("catch", onErr)
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
onResp.Release()
|
onResp.Release()
|
||||||
@@ -75,3 +87,63 @@ func Fetch(url string) (string, error) {
|
|||||||
onErr.Release()
|
onErr.Release()
|
||||||
return body, ferr
|
return body, ferr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchBytes is like Fetch but returns the raw response body as bytes (via
|
||||||
|
// resp.arrayBuffer()), for binary payloads such as gob (Fetch's resp.text()
|
||||||
|
// would corrupt non-UTF-8 data). It blocks the calling goroutine the same way —
|
||||||
|
// call it from a goroutine and write the result into signals when it returns.
|
||||||
|
func FetchBytes(url string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
done = make(chan struct{})
|
||||||
|
body []byte
|
||||||
|
ferr error
|
||||||
|
onResp, onBuf, onErr js.Func
|
||||||
|
)
|
||||||
|
finish := func() { once.Do(func() { close(done) }) }
|
||||||
|
|
||||||
|
onErr = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
msg := "fetch error"
|
||||||
|
if len(args) > 0 && args[0].Truthy() {
|
||||||
|
e := args[0]
|
||||||
|
if m := e.Get("message"); m.Truthy() {
|
||||||
|
msg = m.String()
|
||||||
|
} else {
|
||||||
|
msg = e.Call("toString").String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ferr = errors.New(msg)
|
||||||
|
finish()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
onBuf = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
if len(args) > 0 {
|
||||||
|
arr := js.Global().Get("Uint8Array").New(args[0])
|
||||||
|
b := make([]byte, arr.Get("length").Int())
|
||||||
|
js.CopyBytesToGo(b, arr)
|
||||||
|
body = b
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
onResp = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
resp := args[0]
|
||||||
|
if !resp.Get("ok").Bool() {
|
||||||
|
ferr = fmt.Errorf("HTTP %d", resp.Get("status").Int())
|
||||||
|
finish()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp.Call("arrayBuffer").Call("then", onBuf).Call("catch", onErr)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
js.Global().Call("fetch", absURL(url)).Call("then", onResp).Call("catch", onErr)
|
||||||
|
|
||||||
|
<-done
|
||||||
|
onResp.Release()
|
||||||
|
onBuf.Release()
|
||||||
|
onErr.Release()
|
||||||
|
return body, ferr
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user