//go:build js && wasm package wasmruntime import ( "errors" "fmt" "strings" "sync" "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 // 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 // settles. // // Because it blocks, call it from a goroutine — never from a reactive callback // or event handler — and write the result into signals when it returns, e.g.: // // go func() { // body, err := Fetch(url) // ... // data.Set(...) // updates the UI reactively // }() func Fetch(url string) (string, error) { var ( once sync.Once done = make(chan struct{}) body string ferr error onResp, onText, 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 }) onText = js.FuncOf(func(this js.Value, args []js.Value) any { if len(args) > 0 { body = args[0].String() } 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.text() -> Promise resp.Call("text").Call("then", onText).Call("catch", onErr) return nil }) js.Global().Call("fetch", absURL(url)).Call("then", onResp).Call("catch", onErr) <-done onResp.Release() onText.Release() onErr.Release() 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 }