Skip to content

Commit e09e295

Browse files
authored
Merge pull request #141 from nhooyr/wasm
Implement low level typed interface to the browser WebSocket API
2 parents c940904 + 128b1a2 commit e09e295

File tree

8 files changed

+218
-12
lines changed

8 files changed

+218
-12
lines changed

.github/workflows/ci.yml

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,35 @@ on: [push]
44
jobs:
55
fmt:
66
runs-on: ubuntu-latest
7-
container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc
7+
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
88
steps:
99
- uses: actions/checkout@v1
1010
with:
1111
fetch-depth: 1
1212
- run: ./ci/fmt.sh
1313
lint:
1414
runs-on: ubuntu-latest
15-
container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc
15+
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
1616
steps:
1717
- uses: actions/checkout@v1
1818
with:
1919
fetch-depth: 1
2020
- run: ./ci/lint.sh
2121
test:
2222
runs-on: ubuntu-latest
23-
container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc
23+
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
2424
steps:
2525
- uses: actions/checkout@v1
2626
with:
2727
fetch-depth: 1
2828
- run: ./ci/test.sh
2929
env:
3030
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
31+
wasm:
32+
runs-on: ubuntu-latest
33+
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
34+
steps:
35+
- uses: actions/checkout@v1
36+
with:
37+
fetch-depth: 1
38+
- run: ./ci/wasm.sh

ci/image/Dockerfile

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ FROM golang:1
22

33
ENV DEBIAN_FRONTEND=noninteractive
44
ENV GOPATH=/root/gopath
5+
ENV PATH=$GOPATH/bin:$PATH
56
ENV GOFLAGS="-mod=readonly"
67
ENV PAGER=cat
78
ENV CI=true
89

910
RUN apt-get update && \
10-
apt-get install -y shellcheck npm && \
11+
apt-get install -y shellcheck npm chromium && \
1112
npm install -g prettier
1213

14+
# https://github.com/golang/go/wiki/WebAssembly#running-tests-in-the-browser
15+
RUN go get github.com/agnivade/wasmbrowsertest && \
16+
mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec
17+
1318
RUN git config --global color.ui always
1419

15-
# Cache go modules.
20+
# Cache go modules and build cache.
1621
COPY . /tmp/websocket
1722
RUN cd /tmp/websocket && \
18-
go mod download && \
23+
CI= ./ci/run.sh && \
1924
rm -rf /tmp/websocket

ci/run.sh

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ cd "$(git rev-parse --show-toplevel)"
99
./ci/fmt.sh
1010
./ci/lint.sh
1111
./ci/test.sh
12+
./ci/wasm.sh

ci/wasm.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
cd "$(dirname "${0}")"
5+
cd "$(git rev-parse --show-toplevel)"
6+
7+
GOOS=js GOARCH=wasm go vet ./...
8+
go install golang.org/x/lint/golint
9+
# Get passing later.
10+
#GOOS=js GOARCH=wasm golint -set_exit_status ./...
11+
GOOS=js GOARCH=wasm go test ./internal/wsjs

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ require (
1717
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16
1818
go.uber.org/atomic v1.4.0 // indirect
1919
go.uber.org/multierr v1.1.0
20-
golang.org/x/lint v0.0.0-20190409202823-959b441ac422
20+
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac
2121
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
2222
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect
2323
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
24-
golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3
24+
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72
2525
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
2626
gotest.tools/gotestsum v0.3.5
2727
mvdan.cc/sh v2.6.4+incompatible

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
7070
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
7171
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
7272
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
73-
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
74-
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
73+
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
74+
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
7575
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
7676
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
7777
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=
9595
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
9696
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
9797
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
98-
golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 h1:1cLrGl9PL64Mzl9NATDCqFE57dVYwWOkoPXvppEnjO4=
99-
golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
98+
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws=
99+
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
100100
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
101101
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
102102
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=

internal/wsjs/wsjs.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// +build js
2+
3+
// Package wsjs implements typed access to the browser javascript WebSocket API.
4+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
5+
package wsjs
6+
7+
import (
8+
"context"
9+
"syscall/js"
10+
)
11+
12+
func handleJSError(err *error, onErr func()) {
13+
r := recover()
14+
15+
if jsErr, ok := r.(js.Error); ok {
16+
*err = jsErr
17+
18+
if onErr != nil {
19+
onErr()
20+
}
21+
return
22+
}
23+
24+
if r != nil {
25+
panic(r)
26+
}
27+
}
28+
29+
func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err error) {
30+
defer handleJSError(&err, func() {
31+
c = nil
32+
})
33+
34+
jsProtocols := make([]interface{}, len(protocols))
35+
for i, p := range protocols {
36+
jsProtocols[i] = p
37+
}
38+
39+
c = &WebSocket{
40+
v: js.Global().Get("WebSocket").New(url, jsProtocols),
41+
}
42+
43+
c.setBinaryType("arrayBuffer")
44+
45+
c.Extensions = c.v.Get("extensions").String()
46+
c.Protocol = c.v.Get("protocol").String()
47+
c.URL = c.v.Get("url").String()
48+
49+
return c, nil
50+
}
51+
52+
type WebSocket struct {
53+
Extensions string
54+
Protocol string
55+
URL string
56+
57+
v js.Value
58+
}
59+
60+
func (c *WebSocket) setBinaryType(typ string) {
61+
c.v.Set("binaryType", string(typ))
62+
}
63+
64+
func (c *WebSocket) BufferedAmount() uint32 {
65+
return uint32(c.v.Get("bufferedAmount").Int())
66+
}
67+
68+
func (c *WebSocket) addEventListener(eventType string, fn func(e js.Value)) {
69+
c.v.Call("addEventListener", eventType, js.FuncOf(func(this js.Value, args []js.Value) interface{} {
70+
fn(args[0])
71+
return nil
72+
}))
73+
}
74+
75+
type CloseEvent struct {
76+
Code uint16
77+
Reason string
78+
WasClean bool
79+
}
80+
81+
func (c *WebSocket) OnClose(fn func(CloseEvent)) {
82+
c.addEventListener("close", func(e js.Value) {
83+
ce := CloseEvent{
84+
Code: uint16(e.Get("code").Int()),
85+
Reason: e.Get("reason").String(),
86+
WasClean: e.Get("wasClean").Bool(),
87+
}
88+
fn(ce)
89+
})
90+
}
91+
92+
func (c *WebSocket) OnError(fn func(e js.Value)) {
93+
c.addEventListener("error", fn)
94+
}
95+
96+
type MessageEvent struct {
97+
Data []byte
98+
// There are more types to the interface but we don't use them.
99+
// See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
100+
}
101+
102+
func (c *WebSocket) OnMessage(fn func(m MessageEvent)) {
103+
c.addEventListener("message", func(e js.Value) {
104+
var data []byte
105+
106+
arrayBuffer := e.Get("data")
107+
if arrayBuffer.Type() == js.TypeString {
108+
data = []byte(arrayBuffer.String())
109+
} else {
110+
data = extractArrayBuffer(arrayBuffer)
111+
}
112+
113+
me := MessageEvent{
114+
Data: data,
115+
}
116+
fn(me)
117+
118+
return
119+
})
120+
}
121+
122+
func (c *WebSocket) OnOpen(fn func(e js.Value)) {
123+
c.addEventListener("open", fn)
124+
}
125+
126+
func (c *WebSocket) Close(code int, reason string) (err error) {
127+
defer handleJSError(&err, nil)
128+
c.v.Call("close", code, reason)
129+
return err
130+
}
131+
132+
func (c *WebSocket) SendText(v string) (err error) {
133+
defer handleJSError(&err, nil)
134+
c.v.Call("send", v)
135+
return err
136+
}
137+
138+
func (c *WebSocket) SendBytes(v []byte) (err error) {
139+
defer handleJSError(&err, nil)
140+
c.v.Call("send", uint8Array(v))
141+
return err
142+
}
143+
144+
func extractArrayBuffer(arrayBuffer js.Value) []byte {
145+
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
146+
dst := make([]byte, uint8Array.Length())
147+
js.CopyBytesToGo(dst, uint8Array)
148+
return dst
149+
}
150+
151+
func uint8Array(src []byte) js.Value {
152+
uint8Array := js.Global().Get("Uint8Array").New(len(src))
153+
js.CopyBytesToJS(uint8Array, src)
154+
return uint8Array
155+
}

internal/wsjs/wsjs_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// +build js
2+
3+
package wsjs
4+
5+
import (
6+
"context"
7+
"syscall/js"
8+
"testing"
9+
"time"
10+
)
11+
12+
func TestWebSocket(t *testing.T) {
13+
t.Parallel()
14+
15+
c, err := New(context.Background(), "ws://localhost:8081", nil)
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
20+
c.OnError(func(e js.Value) {
21+
t.Log(js.Global().Get("JSON").Call("stringify", e))
22+
t.Log(c.v.Get("readyState"))
23+
})
24+
25+
time.Sleep(time.Second)
26+
}

0 commit comments

Comments
 (0)