From b31231ab0acecffd5e8644eb9baf9768fc624bd2 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst Date: Mon, 28 May 2018 11:41:53 +0100 Subject: [PATCH] Update fetch testing --- Gopkg.lock | 4 +- fetch/main.go | 7 +- html/go_js_wasm_exec | 14 + html/index.html | 30 -- html/wasm_exec.html | 42 +++ html/wasm_exec.js | 282 ++++++++-------- main.go | 16 + .../johanbrandhorst/fetch/README.md | 29 +- .../github.com/johanbrandhorst/fetch/fetch.go | 306 ++++++++++++------ 9 files changed, 449 insertions(+), 281 deletions(-) create mode 100755 html/go_js_wasm_exec delete mode 100644 html/index.html create mode 100644 html/wasm_exec.html mode change 100644 => 100755 html/wasm_exec.js create mode 100644 main.go diff --git a/Gopkg.lock b/Gopkg.lock index 46feb9c..a1a2510 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -35,7 +35,7 @@ branch = "master" name = "github.com/johanbrandhorst/fetch" packages = ["."] - revision = "94408375f82e811ca0cd2c812adbef2c0aa52d60" + revision = "6a24c959938ff60f3daf26d7a7dc21e363a8470a" [[projects]] name = "github.com/lpar/gzipped" @@ -157,6 +157,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4b10c16cd59cc3ee140eec982a27597b392e754644243d95edafca2ac7729333" + inputs-digest = "3c3a60294951567756e771a7e00b7ef536766428e1e0f076dbf2bed4df73411f" solver-name = "gps-cdcl" solver-version = 1 diff --git a/fetch/main.go b/fetch/main.go index 51cfa20..78fb713 100644 --- a/fetch/main.go +++ b/fetch/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/johanbrandhorst/fetch" ) @@ -14,7 +15,11 @@ func main() { c := http.Client{ Transport: &fetch.Transport{}, } - resp, err := c.Get("https://api.github.com") + resp, err := c.Post( + "https://httpbin.org/anything", + "application/json", + strings.NewReader(`{"test":"test"}`), + ) if err != nil { fmt.Println(err) return diff --git a/html/go_js_wasm_exec b/html/go_js_wasm_exec new file mode 100755 index 0000000..b700722 --- /dev/null +++ b/html/go_js_wasm_exec @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright 2018 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" +done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + +exec node "$DIR/wasm_exec.js" "$@" diff --git a/html/index.html b/html/index.html deleted file mode 100644 index 66275c2..0000000 --- a/html/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Go wasm - - - - - - - - - - diff --git a/html/wasm_exec.html b/html/wasm_exec.html new file mode 100644 index 0000000..cc37ea7 --- /dev/null +++ b/html/wasm_exec.html @@ -0,0 +1,42 @@ + + + + + + + Go wasm + + + + + + + + + + \ No newline at end of file diff --git a/html/wasm_exec.js b/html/wasm_exec.js old mode 100644 new mode 100755 index 5628feb..433ae6d --- a/html/wasm_exec.js +++ b/html/wasm_exec.js @@ -2,20 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -(function () { - let args = ["js"]; - +(() => { // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). const isNodeJS = typeof process !== "undefined"; if (isNodeJS) { - if (process.argv.length < 3) { - process.stderr.write("usage: go_js_wasm_exec [wasm binary]\n"); - process.exit(1); - } - - args = args.concat(process.argv.slice(3)); global.require = require; - global.fs = require("fs"); const nodeCrypto = require("crypto"); @@ -40,15 +31,6 @@ } else { window.global = window; - global.process = { - env: {}, - exit(code) { - if (code !== 0) { - console.warn("exit code:", code); - } - }, - }; - let outputBuf = ""; global.fs = { constants: {}, @@ -67,154 +49,153 @@ const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); - let mod, inst; - let values = []; // TODO: garbage collection - let resolveResume = function () { }; + global.Go = class { + constructor() { + this.argv = []; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; - function mem() { - // The buffer may change when requesting more memory. - return new DataView(inst.exports.mem.buffer); - } + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.mem.buffer); + } - function setInt64(addr, v) { - mem().setUint32(addr + 0, v, true); - if (v >= 0) { - mem().setUint32(addr + 4, v / 4294967296, true); - } else { - mem().setUint32(addr + 4, -1, true); // FIXME - } - } + const setInt64 = (addr, v) => { + mem().setUint32(addr + 0, v, true); + mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); + } - function getInt64(addr) { - const low = mem().getUint32(addr + 0, true); - const high = mem().getInt32(addr + 4, true); - return low + high * 4294967296; - } + const getInt64 = (addr) => { + const low = mem().getUint32(addr + 0, true); + const high = mem().getInt32(addr + 4, true); + return low + high * 4294967296; + } - function loadValue(addr) { - const id = mem().getUint32(addr, true); - return values[id]; - } + const loadValue = (addr) => { + const id = mem().getUint32(addr, true); + return this._values[id]; + } - function storeValue(addr, v) { - if (v === undefined) { - mem().setUint32(addr, 0, true); - return; - } - if (v === null) { - mem().setUint32(addr, 1, true); - return; - } - values.push(v); - mem().setUint32(addr, values.length - 1, true); - } + const storeValue = (addr, v) => { + if (v === undefined) { + mem().setUint32(addr, 0, true); + return; + } + if (v === null) { + mem().setUint32(addr, 1, true); + return; + } + this._values.push(v); + mem().setUint32(addr, this._values.length - 1, true); + } - function loadSlice(addr) { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - return new Uint8Array(inst.exports.mem.buffer, array, len); - } + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } - function loadSliceOfValues(addr) { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - const a = new Array(len); - for (let i = 0; i < len; i++) { - const id = mem().getUint32(array + i * 4, true); - a[i] = values[id]; - } - return a; - } + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + const id = mem().getUint32(array + i * 4, true); + a[i] = this._values[id]; + } + return a; + } - function loadString(addr) { - const saddr = getInt64(addr + 0); - const len = getInt64(addr + 8); - return decoder.decode(new DataView(inst.exports.mem.buffer, saddr, len)); - } + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } - global.go = { - exited: false, - - compileAndRun: async function (source) { - await go.compile(source); - await go.run(); - }, - - compile: async function (source) { - mod = await WebAssembly.compile(source); - }, - - run: async function () { - let importObject = { + this.importObject = { go: { // func wasmExit(code int32) - "runtime.wasmExit": function (sp) { - go.exited = true; - process.exit(mem().getInt32(sp + 8, true)); + "runtime.wasmExit": (sp) => { + this.exited = true; + this.exit(mem().getInt32(sp + 8, true)); }, // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) - "runtime.wasmWrite": function (sp) { + "runtime.wasmWrite": (sp) => { const fd = getInt64(sp + 8); const p = getInt64(sp + 16); const n = mem().getInt32(sp + 24, true); - fs.writeSync(fd, new Uint8Array(inst.exports.mem.buffer, p, n)); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); }, // func nanotime() int64 - "runtime.nanotime": function (sp) { + "runtime.nanotime": (sp) => { setInt64(sp + 8, (performance.timeOrigin + performance.now()) * 1000000); }, // func walltime() (sec int64, nsec int32) - "runtime.walltime": function (sp) { + "runtime.walltime": (sp) => { const msec = (new Date).getTime(); setInt64(sp + 8, msec / 1000); mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); }, + // func scheduleCallback(delay int64) + "runtime.scheduleCallback": (sp) => { + setTimeout(() => { this._resolveCallbackPromise(); }, getInt64(sp + 8)); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + crypto.getRandomValues(loadSlice(sp + 8)); + }, + // func boolVal(value bool) Value - "syscall/js.boolVal": function (sp) { + "syscall/js.boolVal": (sp) => { storeValue(sp + 16, mem().getUint8(sp + 8) !== 0); }, // func intVal(value int) Value - "syscall/js.intVal": function (sp) { + "syscall/js.intVal": (sp) => { storeValue(sp + 16, getInt64(sp + 8)); }, // func floatVal(value float64) Value - "syscall/js.floatVal": function (sp) { + "syscall/js.floatVal": (sp) => { storeValue(sp + 16, mem().getFloat64(sp + 8, true)); }, // func stringVal(value string) Value - "syscall/js.stringVal": function (sp) { + "syscall/js.stringVal": (sp) => { storeValue(sp + 24, loadString(sp + 8)); }, // func (v Value) Get(key string) Value - "syscall/js.Value.Get": function (sp) { + "syscall/js.Value.Get": (sp) => { storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16))); }, // func (v Value) set(key string, value Value) - "syscall/js.Value.set": function (sp) { + "syscall/js.Value.set": (sp) => { Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); }, // func (v Value) Index(i int) Value - "syscall/js.Value.Index": function (sp) { + "syscall/js.Value.Index": (sp) => { storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); }, // func (v Value) setIndex(i int, value Value) - "syscall/js.Value.setIndex": function (sp) { + "syscall/js.Value.setIndex": (sp) => { Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); }, // func (v Value) call(name string, args []Value) (Value, bool) - "syscall/js.Value.call": function (sp) { + "syscall/js.Value.call": (sp) => { try { const v = loadValue(sp + 8); const m = Reflect.get(v, loadString(sp + 16)); @@ -228,7 +209,7 @@ }, // func (v Value) invoke(args []Value) (Value, bool) - "syscall/js.Value.invoke": function (sp) { + "syscall/js.Value.invoke": (sp) => { try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); @@ -241,7 +222,7 @@ }, // func (v Value) new(args []Value) (Value, bool) - "syscall/js.Value.new": function (sp) { + "syscall/js.Value.new": (sp) => { try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); @@ -254,102 +235,125 @@ }, // func (v Value) Float() float64 - "syscall/js.Value.Float": function (sp) { + "syscall/js.Value.Float": (sp) => { mem().setFloat64(sp + 16, parseFloat(loadValue(sp + 8)), true); }, // func (v Value) Int() int - "syscall/js.Value.Int": function (sp) { + "syscall/js.Value.Int": (sp) => { setInt64(sp + 16, parseInt(loadValue(sp + 8))); }, // func (v Value) Bool() bool - "syscall/js.Value.Bool": function (sp) { + "syscall/js.Value.Bool": (sp) => { mem().setUint8(sp + 16, !!loadValue(sp + 8)); }, // func (v Value) Length() int - "syscall/js.Value.Length": function (sp) { + "syscall/js.Value.Length": (sp) => { setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); }, // func (v Value) prepareString() (Value, int) - "syscall/js.Value.prepareString": function (sp) { + "syscall/js.Value.prepareString": (sp) => { const str = encoder.encode(String(loadValue(sp + 8))); storeValue(sp + 16, str); setInt64(sp + 24, str.length); }, // func (v Value) loadString(b []byte) - "syscall/js.Value.loadString": function (sp) { + "syscall/js.Value.loadString": (sp) => { const str = loadValue(sp + 8); loadSlice(sp + 16).set(str); }, - "debug": function (value) { + "debug": (value) => { console.log(value); }, } }; + } - inst = await WebAssembly.instantiate(mod, importObject); - values = [ + async run(instance) { + this._inst = instance; + this._values = [ // TODO: garbage collection undefined, null, global, - inst.exports.mem, - function () { resolveResume(); }, + this._inst.exports.mem, + () => { + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous + }, ]; + this.exited = false; + + const mem = new DataView(this._inst.exports.mem.buffer) // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. let offset = 4096; const strPtr = (str) => { let ptr = offset; - new Uint8Array(inst.exports.mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); + new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); offset += str.length + (8 - (str.length % 8)); return ptr; }; - const argc = args.length; + const argc = this.argv.length; const argvPtrs = []; - args.forEach((arg) => { + this.argv.forEach((arg) => { argvPtrs.push(strPtr(arg)); }); - const keys = Object.keys(process.env).sort(); + const keys = Object.keys(this.env).sort(); argvPtrs.push(keys.length); keys.forEach((key) => { - argvPtrs.push(strPtr(`${key}=${process.env[key]}`)); + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); }); const argv = offset; argvPtrs.forEach((ptr) => { - mem().setUint32(offset, ptr, true); - mem().setUint32(offset + 4, 0, true); + mem.setUint32(offset, ptr, true); + mem.setUint32(offset + 4, 0, true); offset += 8; }); - try { - while (true) { - inst.exports.run(argc, argv); - if (go.exited) { - break; - } - await new Promise((resolve) => { - resolveResume = resolve; - }); + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = resolve; + }); + this._inst.exports.run(argc, argv); + if (this.exited) { + break; } - } catch (err) { - console.error(err); - process.exit(1); + await callbackPromise; } - }, + } } if (isNodeJS) { - go.compileAndRun(fs.readFileSync(process.argv[2])).catch((err) => { + if (process.argv.length < 3) { + process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); + process.exit(1); + } + + const go = new Go(); + go.argv = process.argv.slice(2); + go.env = process.env; + go.exit = process.exit; + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", () => { // Node.js exits if no callback is pending + if (!go.exited) { + console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!"); + process.exit(1); + } + }); + return go.run(result.instance); + }).catch((err) => { console.error(err); process.exit(1); }); diff --git a/main.go b/main.go new file mode 100644 index 0000000..a3d0d17 --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "net/http" +) + +func main() { + fs := http.FileServer(http.Dir("./html")) + http.ListenAndServe(":8080", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/test.wasm" { + resp.Header().Set("content-type", "application/wasm") + } + + fs.ServeHTTP(resp, req) + })) +} diff --git a/vendor/github.com/johanbrandhorst/fetch/README.md b/vendor/github.com/johanbrandhorst/fetch/README.md index 0055fab..5c48802 100644 --- a/vendor/github.com/johanbrandhorst/fetch/README.md +++ b/vendor/github.com/johanbrandhorst/fetch/README.md @@ -4,5 +4,30 @@ The Go http.Transport interface implemented over the WHATWG Fetch API using the ## Usage This package requires the Go WASM compilation target to be supported. -See [my wasm-experiments repo](github.com/johanbrandhorst/wasm-experiments) -for and example of its use. +Short example: + +```go +c := http.Client{ + Transport: &fetch.Transport{}, +} +resp, err := c.Get("https://api.github.com") +if err != nil { + fmt.Println(err) + return +} +defer resp.Body.Close() +b, err := ioutil.ReadAll(resp.Body) +if err != nil { + fmt.Println(err) + return +} +fmt.Println(string(b)) +``` + +See [my wasm-experiments repo](https://github.com/johanbrandhorst/wasm-experiments) +for the full example of its use. + +## Attribution + +The code is largely based on the Fetch API implementation +[in GopherJS](https://github.com/gopherjs/gopherjs/blob/8dffc02ea1cb8398bb73f30424697c60fcf8d4c5/compiler/natives/src/net/http/fetch.go). diff --git a/vendor/github.com/johanbrandhorst/fetch/fetch.go b/vendor/github.com/johanbrandhorst/fetch/fetch.go index bbc51c0..e8bd10b 100644 --- a/vendor/github.com/johanbrandhorst/fetch/fetch.go +++ b/vendor/github.com/johanbrandhorst/fetch/fetch.go @@ -15,6 +15,124 @@ import ( // Adapted for syscall/js from // https://github.com/gopherjs/gopherjs/blob/8dffc02ea1cb8398bb73f30424697c60fcf8d4c5/compiler/natives/src/net/http/fetch.go +// Transport is a RoundTripper that is implemented using the WHATWG Fetch API. +// It supports streaming response bodies. +type Transport struct{} + +// RoundTrip performs a full round trip of a request. +func (*Transport) RoundTrip(req *http.Request) (*http.Response, error) { + headers := js.Global.Get("Headers").New() + for key, values := range req.Header { + for _, value := range values { + headers.Call("append", key, value) + } + } + + ac := js.Global.Get("AbortController").New() + + opt := js.Global.Get("Object").New() + opt.Set("headers", headers) + opt.Set("method", req.Method) + opt.Set("credentials", "same-origin") + opt.Set("signal", ac.Get("signal")) + + var ( + respCh = make(chan *http.Response, 1) + errCh = make(chan error, 1) + ) + if req.Body != nil { + /* Streaming request bodies are not supported yet + body := js.Global.Get("ReadableStream") + if body != js.Undefined { + source := js.Global.Get("Object").New() + // TODO(johanbrandhorst): Use ReadableByteStreamController. + // Currently Unsupported: https://developer.mozilla.org/en-US/docs/Web/API/ReadableByteStreamController#Browser_Compatibility + start := js.NewCallback(func(args []js.Value) { + fmt.Println("start called") + controller := args[0] + w := &streamWriter{controller: controller} + _, err := io.Copy(w, req.Body) + if err != nil { + errCh <- err + return + } + }) + defer start.Close() + source.Set("start", start) + body = js.Global.Get("ReadableStream").New(source) + } + */ + content, err := ioutil.ReadAll(req.Body) + if err != nil { + req.Body.Close() // RoundTrip must always close the body, including on errors. + return nil, err + } + req.Body.Close() + opt.Set("body", js.ValueOf(content)) + } + respPromise := js.Global.Call("fetch", req.URL.String(), opt) + if respPromise == js.Undefined { + return nil, errors.New("your browser does not support the Fetch API, please upgrade") + } + + success := js.NewCallback(func(args []js.Value) { + result := args[0] + header := http.Header{} + writeHeaders := js.NewCallback(func(args []js.Value) { + key, value := args[0].String(), args[1].String() + ck := http.CanonicalHeaderKey(key) + header[ck] = append(header[ck], value) + }) + defer writeHeaders.Close() + result.Get("headers").Call("forEach", writeHeaders) + + contentLength := int64(-1) + if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil { + contentLength = cl + } + + b := result.Get("body") + var body io.ReadCloser + if b != js.Undefined { + body = &streamReader{stream: b.Call("getReader")} + } else { + // Fall back to using the arrayBuffer + // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer + body = &arrayReader{arrayPromise: result.Call("arrayBuffer")} + } + select { + case respCh <- &http.Response{ + Status: result.Get("status").String() + " " + http.StatusText(result.Get("status").Int()), + StatusCode: result.Get("status").Int(), + Header: header, + ContentLength: contentLength, + Body: body, + Request: req, + }: + case <-req.Context().Done(): + } + }) + defer success.Close() + failure := js.NewCallback(func(args []js.Value) { + select { + case errCh <- fmt.Errorf("net/http: fetch() failed: %s", args[0].String()): + case <-req.Context().Done(): + } + }) + defer failure.Close() + respPromise.Call("then", success, failure) + select { + case <-req.Context().Done(): + // Abort the Fetch request + ac.Call("abort") + return nil, errors.New("net/http: request canceled") + case resp := <-respCh: + return resp, nil + case err := <-errCh: + return nil, err + } +} + // streamReader implements an io.ReadCloser wrapper for ReadableStream of https://fetch.spec.whatwg.org/. type streamReader struct { pending []byte @@ -24,25 +142,24 @@ type streamReader struct { func (r *streamReader) Read(p []byte) (n int, err error) { if len(r.pending) == 0 { var ( - bCh = make(chan []byte) - errCh = make(chan error) - ) - r.stream.Call("read").Call("then", - js.NewCallback(func(args []js.Value) { - result := args[0] - if result.Get("done").Bool() { - errCh <- io.EOF - return - } - value := make([]byte, result.Get("value").Get("byteLength").Int()) - js.ValueOf(value).Call("set", result.Get("value")) - bCh <- value - }), - js.NewCallback(func(args []js.Value) { - // Assumes it's a DOMException. - errCh <- errors.New(args[0].Get("message").String()) - }), + bCh = make(chan []byte, 1) + errCh = make(chan error, 1) ) + success := js.NewCallback(func(args []js.Value) { + result := args[0] + if result.Get("done").Bool() { + errCh <- io.EOF + return + } + bCh <- copyBytes(result.Get("value")) + }) + defer success.Close() + failure := js.NewCallback(func(args []js.Value) { + // Assumes it's a DOMException. + errCh <- errors.New(args[0].Get("message").String()) + }) + defer failure.Close() + r.stream.Call("read").Call("then", success, failure) select { case b := <-bCh: r.pending = b @@ -63,93 +180,68 @@ func (r *streamReader) Close() error { return nil } -// Transport is a RoundTripper that is implemented using the WHATWG Fetch API. -// It supports streaming response bodies. -type Transport struct{} - -// RoundTrip performs a full round trip of a request. -func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - headers := js.Global.Get("Headers").New() - for key, values := range req.Header { - for _, value := range values { - headers.Call("append", key, value) - } - } - - ac := js.Global.Get("AbortController").New() - - opt := js.Global.Get("Object").New() - opt.Set("headers", headers) - opt.Set("method", req.Method) - opt.Set("credentials", "same-origin") - opt.Set("signal", ac.Get("signal")) - - if req.Body != nil { - body, err := ioutil.ReadAll(req.Body) - if err != nil { - _ = req.Body.Close() // RoundTrip must always close the body, including on errors. - return nil, err - } - _ = req.Body.Close() - opt.Set("body", body) - } - respPromise := js.Global.Call("fetch", req.URL.String(), opt) - if respPromise == js.Undefined { - return nil, errors.New("your browser does not support the Fetch API, please upgrade") - } - - var ( - respCh = make(chan *http.Response) - errCh = make(chan error) - ) - respPromise.Call("then", - js.NewCallback(func(args []js.Value) { - result := args[0] - header := http.Header{} - result.Get("headers").Call("forEach", js.NewCallback(func(args []js.Value) { - key, value := args[0].String(), args[1].String() - ck := http.CanonicalHeaderKey(key) - header[ck] = append(header[ck], value) - })) - - contentLength := int64(-1) - if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil { - contentLength = cl - } - - b := result.Get("body") - if b == js.Undefined { - errCh <- errors.New("your browser does not support the ReadableStream API, please upgrade") - return - } - - select { - case respCh <- &http.Response{ - Status: result.Get("status").String() + " " + http.StatusText(result.Get("status").Int()), - StatusCode: result.Get("status").Int(), - Header: header, - ContentLength: contentLength, - Body: &streamReader{stream: b.Call("getReader")}, - Request: req, - }: - case <-req.Context().Done(): - } - }), - js.NewCallback(func(args []js.Value) { - select { - case errCh <- fmt.Errorf("net/http: fetch() failed: %s", args[0].String()): - case <-req.Context().Done(): - } - }), - ) - select { - case <-req.Context().Done(): - // Abort the Fetch request - ac.Call("abort") - return nil, errors.New("net/http: request canceled") - case resp := <-respCh: - return resp, nil - case err := <-errCh: - return nil, err - } +// arrayReader implements an io.ReadCloser wrapper for arrayBuffer +// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer. +type arrayReader struct { + arrayPromise js.Value + pending []byte + read bool +} + +func (r *arrayReader) Read(p []byte) (n int, err error) { + if !r.read { + r.read = true + var ( + bCh = make(chan []byte, 1) + errCh = make(chan error, 1) + ) + success := js.NewCallback(func(args []js.Value) { + // Wrap the input ArrayBuffer with a Uint8Array + uint8arrayWrapper := js.Global.Get("Uint8Array").New(args[0]) + bCh <- copyBytes(uint8arrayWrapper) + }) + defer success.Close() + failure := js.NewCallback(func(args []js.Value) { + // Assumes it's a DOMException. + errCh <- errors.New(args[0].Get("message").String()) + }) + defer failure.Close() + r.arrayPromise.Call("then", success, failure) + select { + case b := <-bCh: + r.pending = b + case err := <-errCh: + return 0, err + } + } + if len(r.pending) == 0 { + return 0, io.EOF + } + n = copy(p, r.pending) + r.pending = r.pending[n:] + return n, nil +} + +func (r *arrayReader) Close() error { + // This is a noop + return nil +} + +/* +// streamWriter exposes a ReadableStreamDefaultController as an io.Writer +// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController +type streamWriter struct { + controller js.Value +} + +func (w *streamWriter) Write(p []byte) (int, error) { + w.controller.Call("enqueue", p) + return len(p), nil +} +*/ + +func copyBytes(in js.Value) []byte { + value := make([]byte, in.Get("byteLength").Int()) + js.ValueOf(value).Call("set", in) + return value }