Skip to content

Implement core API for WASM #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ on: [push]
jobs:
fmt:
runs-on: ubuntu-latest
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- run: ./ci/fmt.sh
lint:
runs-on: ubuntu-latest
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- run: ./ci/lint.sh
test:
runs-on: ubuntu-latest
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
steps:
- uses: actions/checkout@v1
with:
Expand All @@ -30,7 +30,7 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
wasm:
runs-on: ubuntu-latest
container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75
container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f
steps:
- uses: actions/checkout@v1
with:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ go get nhooyr.io/websocket
## Features

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This is an abbreviation (not an acronym) so it's usually spelled "Wasm".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


## Roadmap

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

## Examples

Expand Down Expand Up @@ -115,7 +115,7 @@ Just compare the godoc of

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

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

Additionally, nhooyr.io/websocket can compile to [WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) for the browser.

In terms of performance, the differences mostly depend on your application code. nhooyr/websocket
reuses message buffers out of the box if you use the wsjson and wspb subpackages.
As mentioned above, nhooyr/websocket also supports concurrent writers.
Expand Down
8 changes: 8 additions & 0 deletions accept.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -41,6 +43,12 @@ type AcceptOptions struct {
}

func verifyClientRequest(w http.ResponseWriter, r *http.Request) error {
if !r.ProtoAtLeast(1, 1) {
err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
http.Error(w, err.Error(), http.StatusBadRequest)
return err
}

if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") {
err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
http.Error(w, err.Error(), http.StatusBadRequest)
Expand Down
19 changes: 19 additions & 0 deletions accept_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -45,6 +47,7 @@ func Test_verifyClientHandshake(t *testing.T) {
testCases := []struct {
name string
method string
http1 bool
h map[string]string
success bool
}{
Expand Down Expand Up @@ -86,6 +89,16 @@ func Test_verifyClientHandshake(t *testing.T) {
"Sec-WebSocket-Key": "",
},
},
{
name: "badHTTPVersion",
h: map[string]string{
"Connection": "Upgrade",
"Upgrade": "websocket",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Key": "meow123",
},
http1: true,
},
{
name: "success",
h: map[string]string{
Expand All @@ -106,6 +119,12 @@ func Test_verifyClientHandshake(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(tc.method, "/", nil)

r.ProtoMajor = 1
r.ProtoMinor = 1
if tc.http1 {
r.ProtoMinor = 0
}

for k, v := range tc.h {
r.Header.Set(k, v)
}
Expand Down
124 changes: 124 additions & 0 deletions assert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package websocket_test

import (
"context"
"encoding/hex"
"fmt"
"math/rand"
"reflect"

"github.com/google/go-cmp/cmp"

"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
)

// https://github.com/google/go-cmp/issues/40#issuecomment-328615283
func cmpDiff(exp, act interface{}) string {
return cmp.Diff(exp, act, deepAllowUnexported(exp, act))
}

func deepAllowUnexported(vs ...interface{}) cmp.Option {
m := make(map[reflect.Type]struct{})
for _, v := range vs {
structTypes(reflect.ValueOf(v), m)
}
var typs []interface{}
for t := range m {
typs = append(typs, reflect.New(t).Elem().Interface())
}
return cmp.AllowUnexported(typs...)
}

func structTypes(v reflect.Value, m map[reflect.Type]struct{}) {
if !v.IsValid() {
return
}
switch v.Kind() {
case reflect.Ptr:
if !v.IsNil() {
structTypes(v.Elem(), m)
}
case reflect.Interface:
if !v.IsNil() {
structTypes(v.Elem(), m)
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
structTypes(v.Index(i), m)
}
case reflect.Map:
for _, k := range v.MapKeys() {
structTypes(v.MapIndex(k), m)
}
case reflect.Struct:
m[v.Type()] = struct{}{}
for i := 0; i < v.NumField(); i++ {
structTypes(v.Field(i), m)
}
}
}

func assertEqualf(exp, act interface{}, f string, v ...interface{}) error {
if diff := cmpDiff(exp, act); diff != "" {
return fmt.Errorf(f+": %v", append(v, diff)...)
}
return nil
}

func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error {
exp := randString(n)
err := wsjson.Write(ctx, c, exp)
if err != nil {
return err
}

var act interface{}
err = wsjson.Read(ctx, c, &act)
if err != nil {
return err
}

return assertEqualf(exp, act, "unexpected JSON")
}

func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error {
var act interface{}
err := wsjson.Read(ctx, c, &act)
if err != nil {
return err
}

return assertEqualf(exp, act, "unexpected JSON")
}

func randBytes(n int) []byte {
b := make([]byte, n)
rand.Read(b)
return b
}

func randString(n int) string {
return hex.EncodeToString(randBytes(n))[:n]
}

func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error {
p := randBytes(n)
err := c.Write(ctx, typ, p)
if err != nil {
return err
}
typ2, p2, err := c.Read(ctx)
if err != nil {
return err
}
err = assertEqualf(typ, typ2, "unexpected data type")
if err != nil {
return err
}
return assertEqualf(p, p2, "unexpected payload")
}

func assertSubprotocol(c *websocket.Conn, exp string) error {
return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol")
}
2 changes: 1 addition & 1 deletion ci/fmt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fmt() {
go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .
go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr .
# shellcheck disable=SC2046
npx prettier \
npx -q prettier \
--write \
--print-width 120 \
--no-semi \
Expand Down
7 changes: 7 additions & 0 deletions ci/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ set -euo pipefail
cd "$(dirname "${0}")"
cd "$(git rev-parse --show-toplevel)"

echo "--- fmt"
./ci/fmt.sh

echo "--- lint"
./ci/lint.sh

echo "--- test"
./ci/test.sh

echo "--- wasm"
./ci/wasm.sh
10 changes: 7 additions & 3 deletions ci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ if [[ ${CI-} ]]; then
)
fi

argv+=(
"$@"
)
if [[ $# -gt 0 ]]; then
argv+=(
"$@"
)
else
argv+=(./...)
fi

mkdir -p ci/out/websocket
"${argv[@]}"
Expand Down
1 change: 1 addition & 0 deletions ci/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package ci

// See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md
import (
_ "github.com/agnivade/wasmbrowsertest"
_ "go.coder.com/go-tools/cmd/goimports"
_ "golang.org/x/lint/golint"
_ "golang.org/x/tools/cmd/stringer"
Expand Down
25 changes: 22 additions & 3 deletions ci/wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@ 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
GOOS=js GOARCH=wasm golint -set_exit_status ./...

wsjstestOut="$(mktemp -d)/stdout"
mkfifo "$wsjstestOut"
go install ./internal/wsjstest
timeout 30s wsjstest > "$wsjstestOut" &
wsjstestPID=$!

WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsjstestOut")" || true
if [[ -z $WS_ECHO_SERVER_URL ]]; then
echo "./internal/wsjstest failed to start in 10s"
exit 1
fi

go install github.com/agnivade/wasmbrowsertest
GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL"

if ! wait "$wsjstestPID"; then
echo "wsjstest exited unsuccessfully"
exit 1
fi
53 changes: 0 additions & 53 deletions cmp_test.go

This file was deleted.

Loading