Skip to content

Commit 7b4bd30

Browse files
authored
Merge pull request #142 from nhooyr/wasm
Implement core API for WASM
2 parents e09e295 + 76a6a26 commit 7b4bd30

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+875
-236
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ on: [push]
44
jobs:
55
fmt:
66
runs-on: ubuntu-latest
7-
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
7+
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
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:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
15+
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
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:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
23+
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
2424
steps:
2525
- uses: actions/checkout@v1
2626
with:
@@ -30,7 +30,7 @@ jobs:
3030
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
3131
wasm:
3232
runs-on: ubuntu-latest
33-
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
33+
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
3434
steps:
3535
- uses: actions/checkout@v1
3636
with:

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ go get nhooyr.io/websocket
1616
## Features
1717

1818
- Minimal and idiomatic API
19-
- Tiny codebase at 1700 lines
19+
- Tiny codebase at 2200 lines
2020
- First class [context.Context](https://blog.golang.org/context) support
2121
- Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
2222
- [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports)
2323
- JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages
2424
- Highly optimized by default
2525
- Concurrent writes out of the box
26+
- [Complete WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) support
2627

2728
## Roadmap
2829

2930
- [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
30-
- [ ] WASM Compilation [#121](https://github.com/nhooyr/websocket/issues/121)
3131

3232
## Examples
3333

@@ -115,7 +115,7 @@ Just compare the godoc of
115115

116116
The API for nhooyr/websocket has been designed such that there is only one way to do things
117117
which makes it easy to use correctly. Not only is the API simpler, the implementation is
118-
only 1700 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain,
118+
only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain,
119119
more code to test, more code to document and more surface area for bugs.
120120

121121
Moreover, nhooyr/websocket has support for newer Go idioms such as context.Context and
@@ -131,6 +131,8 @@ which results in awkward control flow. With nhooyr/websocket you use the Ping me
131131
that sends a ping and also waits for the pong, though you must be reading from the connection
132132
for the pong to be read.
133133

134+
Additionally, nhooyr.io/websocket can compile to [WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) for the browser.
135+
134136
In terms of performance, the differences mostly depend on your application code. nhooyr/websocket
135137
reuses message buffers out of the box if you use the wsjson and wspb subpackages.
136138
As mentioned above, nhooyr/websocket also supports concurrent writers.

accept.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build !js
2+
13
package websocket
24

35
import (
@@ -41,6 +43,12 @@ type AcceptOptions struct {
4143
}
4244

4345
func verifyClientRequest(w http.ResponseWriter, r *http.Request) error {
46+
if !r.ProtoAtLeast(1, 1) {
47+
err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
48+
http.Error(w, err.Error(), http.StatusBadRequest)
49+
return err
50+
}
51+
4452
if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") {
4553
err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
4654
http.Error(w, err.Error(), http.StatusBadRequest)

accept_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build !js
2+
13
package websocket
24

35
import (
@@ -45,6 +47,7 @@ func Test_verifyClientHandshake(t *testing.T) {
4547
testCases := []struct {
4648
name string
4749
method string
50+
http1 bool
4851
h map[string]string
4952
success bool
5053
}{
@@ -86,6 +89,16 @@ func Test_verifyClientHandshake(t *testing.T) {
8689
"Sec-WebSocket-Key": "",
8790
},
8891
},
92+
{
93+
name: "badHTTPVersion",
94+
h: map[string]string{
95+
"Connection": "Upgrade",
96+
"Upgrade": "websocket",
97+
"Sec-WebSocket-Version": "13",
98+
"Sec-WebSocket-Key": "meow123",
99+
},
100+
http1: true,
101+
},
89102
{
90103
name: "success",
91104
h: map[string]string{
@@ -106,6 +119,12 @@ func Test_verifyClientHandshake(t *testing.T) {
106119
w := httptest.NewRecorder()
107120
r := httptest.NewRequest(tc.method, "/", nil)
108121

122+
r.ProtoMajor = 1
123+
r.ProtoMinor = 1
124+
if tc.http1 {
125+
r.ProtoMinor = 0
126+
}
127+
109128
for k, v := range tc.h {
110129
r.Header.Set(k, v)
111130
}

assert_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package websocket_test
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"math/rand"
8+
"reflect"
9+
10+
"github.com/google/go-cmp/cmp"
11+
12+
"nhooyr.io/websocket"
13+
"nhooyr.io/websocket/wsjson"
14+
)
15+
16+
// https://github.com/google/go-cmp/issues/40#issuecomment-328615283
17+
func cmpDiff(exp, act interface{}) string {
18+
return cmp.Diff(exp, act, deepAllowUnexported(exp, act))
19+
}
20+
21+
func deepAllowUnexported(vs ...interface{}) cmp.Option {
22+
m := make(map[reflect.Type]struct{})
23+
for _, v := range vs {
24+
structTypes(reflect.ValueOf(v), m)
25+
}
26+
var typs []interface{}
27+
for t := range m {
28+
typs = append(typs, reflect.New(t).Elem().Interface())
29+
}
30+
return cmp.AllowUnexported(typs...)
31+
}
32+
33+
func structTypes(v reflect.Value, m map[reflect.Type]struct{}) {
34+
if !v.IsValid() {
35+
return
36+
}
37+
switch v.Kind() {
38+
case reflect.Ptr:
39+
if !v.IsNil() {
40+
structTypes(v.Elem(), m)
41+
}
42+
case reflect.Interface:
43+
if !v.IsNil() {
44+
structTypes(v.Elem(), m)
45+
}
46+
case reflect.Slice, reflect.Array:
47+
for i := 0; i < v.Len(); i++ {
48+
structTypes(v.Index(i), m)
49+
}
50+
case reflect.Map:
51+
for _, k := range v.MapKeys() {
52+
structTypes(v.MapIndex(k), m)
53+
}
54+
case reflect.Struct:
55+
m[v.Type()] = struct{}{}
56+
for i := 0; i < v.NumField(); i++ {
57+
structTypes(v.Field(i), m)
58+
}
59+
}
60+
}
61+
62+
func assertEqualf(exp, act interface{}, f string, v ...interface{}) error {
63+
if diff := cmpDiff(exp, act); diff != "" {
64+
return fmt.Errorf(f+": %v", append(v, diff)...)
65+
}
66+
return nil
67+
}
68+
69+
func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error {
70+
exp := randString(n)
71+
err := wsjson.Write(ctx, c, exp)
72+
if err != nil {
73+
return err
74+
}
75+
76+
var act interface{}
77+
err = wsjson.Read(ctx, c, &act)
78+
if err != nil {
79+
return err
80+
}
81+
82+
return assertEqualf(exp, act, "unexpected JSON")
83+
}
84+
85+
func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error {
86+
var act interface{}
87+
err := wsjson.Read(ctx, c, &act)
88+
if err != nil {
89+
return err
90+
}
91+
92+
return assertEqualf(exp, act, "unexpected JSON")
93+
}
94+
95+
func randBytes(n int) []byte {
96+
b := make([]byte, n)
97+
rand.Read(b)
98+
return b
99+
}
100+
101+
func randString(n int) string {
102+
return hex.EncodeToString(randBytes(n))[:n]
103+
}
104+
105+
func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error {
106+
p := randBytes(n)
107+
err := c.Write(ctx, typ, p)
108+
if err != nil {
109+
return err
110+
}
111+
typ2, p2, err := c.Read(ctx)
112+
if err != nil {
113+
return err
114+
}
115+
err = assertEqualf(typ, typ2, "unexpected data type")
116+
if err != nil {
117+
return err
118+
}
119+
return assertEqualf(p, p2, "unexpected payload")
120+
}
121+
122+
func assertSubprotocol(c *websocket.Conn, exp string) error {
123+
return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol")
124+
}

ci/fmt.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fmt() {
1818
go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .
1919
go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr .
2020
# shellcheck disable=SC2046
21-
npx prettier \
21+
npx -q prettier \
2222
--write \
2323
--print-width 120 \
2424
--no-semi \

ci/run.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ set -euo pipefail
66
cd "$(dirname "${0}")"
77
cd "$(git rev-parse --show-toplevel)"
88

9+
echo "--- fmt"
910
./ci/fmt.sh
11+
12+
echo "--- lint"
1013
./ci/lint.sh
14+
15+
echo "--- test"
1116
./ci/test.sh
17+
18+
echo "--- wasm"
1219
./ci/wasm.sh

ci/test.sh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ if [[ ${CI-} ]]; then
1616
)
1717
fi
1818

19-
argv+=(
20-
"$@"
21-
)
19+
if [[ $# -gt 0 ]]; then
20+
argv+=(
21+
"$@"
22+
)
23+
else
24+
argv+=(./...)
25+
fi
2226

2327
mkdir -p ci/out/websocket
2428
"${argv[@]}"

ci/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package ci
44

55
// See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md
66
import (
7+
_ "github.com/agnivade/wasmbrowsertest"
78
_ "go.coder.com/go-tools/cmd/goimports"
89
_ "golang.org/x/lint/golint"
910
_ "golang.org/x/tools/cmd/stringer"

ci/wasm.sh

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@ cd "$(dirname "${0}")"
55
cd "$(git rev-parse --show-toplevel)"
66

77
GOOS=js GOARCH=wasm go vet ./...
8+
89
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
10+
GOOS=js GOARCH=wasm golint -set_exit_status ./...
11+
12+
wsjstestOut="$(mktemp -d)/stdout"
13+
mkfifo "$wsjstestOut"
14+
go install ./internal/wsjstest
15+
timeout 30s wsjstest > "$wsjstestOut" &
16+
wsjstestPID=$!
17+
18+
WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsjstestOut")" || true
19+
if [[ -z $WS_ECHO_SERVER_URL ]]; then
20+
echo "./internal/wsjstest failed to start in 10s"
21+
exit 1
22+
fi
23+
24+
go install github.com/agnivade/wasmbrowsertest
25+
GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL"
26+
27+
if ! wait "$wsjstestPID"; then
28+
echo "wsjstest exited unsuccessfully"
29+
exit 1
30+
fi

cmp_test.go

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)