diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e63eb17437587fd110c8d47420f96a607fd9fe3..4d889ab59f67c7156d3952d6a31c7eb293e4080e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -12,7 +12,7 @@ jobs: - run: ./ci/fmt.sh lint: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -20,7 +20,7 @@ jobs: - run: ./ci/lint.sh test: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -28,3 +28,11 @@ jobs: - run: ./ci/test.sh env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + wasm: + runs-on: ubuntu-latest + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: ./ci/wasm.sh diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index b59bc3af55dc1a5404870076fd548a55fa151710..8d79215943a8e63db1b8fed425e7eadcf539b62d 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -2,18 +2,23 @@ FROM golang:1 ENV DEBIAN_FRONTEND=noninteractive ENV GOPATH=/root/gopath +ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true RUN apt-get update && \ - apt-get install -y shellcheck npm && \ + apt-get install -y shellcheck npm chromium && \ npm install -g prettier +# https://github.com/golang/go/wiki/WebAssembly#running-tests-in-the-browser +RUN go get github.com/agnivade/wasmbrowsertest && \ + mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec + RUN git config --global color.ui always -# Cache go modules. +# Cache go modules and build cache. COPY . /tmp/websocket RUN cd /tmp/websocket && \ - go mod download && \ + CI= ./ci/run.sh && \ rm -rf /tmp/websocket diff --git a/ci/run.sh b/ci/run.sh index 8867b860e1525c20989b61685255d4d74b854b14..9e47d29162a605692a31a85c8e07a8836b1d9dd6 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -9,3 +9,4 @@ cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh ./ci/test.sh +./ci/wasm.sh diff --git a/ci/wasm.sh b/ci/wasm.sh new file mode 100755 index 0000000000000000000000000000000000000000..943d380626f6c0ec7d9969aab085236091a93556 --- /dev/null +++ b/ci/wasm.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +cd "$(git rev-parse --show-toplevel)" + +GOOS=js GOARCH=wasm go vet ./... +go install golang.org/x/lint/golint +# Get passing later. +#GOOS=js GOARCH=wasm golint -set_exit_status ./... +GOOS=js GOARCH=wasm go test ./internal/wsjs diff --git a/go.mod b/go.mod index c247f54a110c33fb337653f13ab5bf1f4e105c46..34a7f872d72e550fd0f6945250462f979bf55100 100644 --- a/go.mod +++ b/go.mod @@ -17,11 +17,11 @@ require ( go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 - golang.org/x/lint v0.0.0-20190409202823-959b441ac422 + golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 + golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gotest.tools/gotestsum v0.3.5 mvdan.cc/sh v2.6.4+incompatible diff --git a/go.sum b/go.sum index a6a86413d50efae321b6b9d261442519ccc77d52..97d6a8358df28bc7ea0ca370efcdd26e3cca2489 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -95,8 +95,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 h1:1cLrGl9PL64Mzl9NATDCqFE57dVYwWOkoPXvppEnjO4= -golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/internal/wsjs/wsjs.go b/internal/wsjs/wsjs.go new file mode 100644 index 0000000000000000000000000000000000000000..4adb71ad2072a3d0e27616d4d8906d99d4ee97ff --- /dev/null +++ b/internal/wsjs/wsjs.go @@ -0,0 +1,155 @@ +// +build js + +// Package wsjs implements typed access to the browser javascript WebSocket API. +// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket +package wsjs + +import ( + "context" + "syscall/js" +) + +func handleJSError(err *error, onErr func()) { + r := recover() + + if jsErr, ok := r.(js.Error); ok { + *err = jsErr + + if onErr != nil { + onErr() + } + return + } + + if r != nil { + panic(r) + } +} + +func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err error) { + defer handleJSError(&err, func() { + c = nil + }) + + jsProtocols := make([]interface{}, len(protocols)) + for i, p := range protocols { + jsProtocols[i] = p + } + + c = &WebSocket{ + v: js.Global().Get("WebSocket").New(url, jsProtocols), + } + + c.setBinaryType("arrayBuffer") + + c.Extensions = c.v.Get("extensions").String() + c.Protocol = c.v.Get("protocol").String() + c.URL = c.v.Get("url").String() + + return c, nil +} + +type WebSocket struct { + Extensions string + Protocol string + URL string + + v js.Value +} + +func (c *WebSocket) setBinaryType(typ string) { + c.v.Set("binaryType", string(typ)) +} + +func (c *WebSocket) BufferedAmount() uint32 { + return uint32(c.v.Get("bufferedAmount").Int()) +} + +func (c *WebSocket) addEventListener(eventType string, fn func(e js.Value)) { + c.v.Call("addEventListener", eventType, js.FuncOf(func(this js.Value, args []js.Value) interface{} { + fn(args[0]) + return nil + })) +} + +type CloseEvent struct { + Code uint16 + Reason string + WasClean bool +} + +func (c *WebSocket) OnClose(fn func(CloseEvent)) { + c.addEventListener("close", func(e js.Value) { + ce := CloseEvent{ + Code: uint16(e.Get("code").Int()), + Reason: e.Get("reason").String(), + WasClean: e.Get("wasClean").Bool(), + } + fn(ce) + }) +} + +func (c *WebSocket) OnError(fn func(e js.Value)) { + c.addEventListener("error", fn) +} + +type MessageEvent struct { + Data []byte + // There are more types to the interface but we don't use them. + // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent +} + +func (c *WebSocket) OnMessage(fn func(m MessageEvent)) { + c.addEventListener("message", func(e js.Value) { + var data []byte + + arrayBuffer := e.Get("data") + if arrayBuffer.Type() == js.TypeString { + data = []byte(arrayBuffer.String()) + } else { + data = extractArrayBuffer(arrayBuffer) + } + + me := MessageEvent{ + Data: data, + } + fn(me) + + return + }) +} + +func (c *WebSocket) OnOpen(fn func(e js.Value)) { + c.addEventListener("open", fn) +} + +func (c *WebSocket) Close(code int, reason string) (err error) { + defer handleJSError(&err, nil) + c.v.Call("close", code, reason) + return err +} + +func (c *WebSocket) SendText(v string) (err error) { + defer handleJSError(&err, nil) + c.v.Call("send", v) + return err +} + +func (c *WebSocket) SendBytes(v []byte) (err error) { + defer handleJSError(&err, nil) + c.v.Call("send", uint8Array(v)) + return err +} + +func extractArrayBuffer(arrayBuffer js.Value) []byte { + uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer) + dst := make([]byte, uint8Array.Length()) + js.CopyBytesToGo(dst, uint8Array) + return dst +} + +func uint8Array(src []byte) js.Value { + uint8Array := js.Global().Get("Uint8Array").New(len(src)) + js.CopyBytesToJS(uint8Array, src) + return uint8Array +} diff --git a/internal/wsjs/wsjs_test.go b/internal/wsjs/wsjs_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4f5f18789845d93665d16a72a8a87d8b4f8a3e02 --- /dev/null +++ b/internal/wsjs/wsjs_test.go @@ -0,0 +1,26 @@ +// +build js + +package wsjs + +import ( + "context" + "syscall/js" + "testing" + "time" +) + +func TestWebSocket(t *testing.T) { + t.Parallel() + + c, err := New(context.Background(), "ws://localhost:8081", nil) + if err != nil { + t.Fatal(err) + } + + c.OnError(func(e js.Value) { + t.Log(js.Global().Get("JSON").Call("stringify", e)) + t.Log(c.v.Get("readyState")) + }) + + time.Sleep(time.Second) +}