159 lines
4.3 KiB
Go
159 lines
4.3 KiB
Go
//go:build js && wasm
|
|
|
|
package wasmruntime
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"syscall/js"
|
|
|
|
"kjol/httputil"
|
|
)
|
|
|
|
// Installing the transport here (rather than leaving it to each app's main)
|
|
// keeps httputil's invariant true by construction: FetchGob / FetchJSON have a
|
|
// transport exactly when they are running in the browser. That is what lets them
|
|
// no-op during SSR instead of reporting a failure. Apps that need custom headers
|
|
// or a base URL can wrap FetchBytes with httputil.SetClientTransport at startup.
|
|
func init() { httputil.SetClientTransport(FetchBytes) }
|
|
|
|
// 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<string>
|
|
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
|
|
}
|