From f6a26896c967a2f6e782f1e4a216204cebaa1507 Mon Sep 17 00:00:00 2001
From: Anmol Sethi <hi@nhooyr.io>
Date: Fri, 20 Sep 2019 22:18:29 -0500
Subject: [PATCH] Implement low level typed interface to the browser WebSocket
 API

Updates #121
---
 internal/websocketjs/websocketjs.go      | 149 +++++++++++++++++++++++
 internal/websocketjs/websocketjs_test.go |  26 ++++
 2 files changed, 175 insertions(+)
 create mode 100644 internal/websocketjs/websocketjs.go
 create mode 100644 internal/websocketjs/websocketjs_test.go

diff --git a/internal/websocketjs/websocketjs.go b/internal/websocketjs/websocketjs.go
new file mode 100644
index 0000000..0265956
--- /dev/null
+++ b/internal/websocketjs/websocketjs.go
@@ -0,0 +1,149 @@
+// +build js
+
+// websocketjs implements typed access to the browser javascript WebSocket API.
+// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
+package websocketjs
+
+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) {
+		arrayBuffer := e.Get("data")
+		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/websocketjs/websocketjs_test.go b/internal/websocketjs/websocketjs_test.go
new file mode 100644
index 0000000..46819bd
--- /dev/null
+++ b/internal/websocketjs/websocketjs_test.go
@@ -0,0 +1,26 @@
+// +build js
+
+package websocketjs
+
+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)
+}
-- 
GitLab