Skip to content

Incomplete PR: Update to use gopherwasm/js for cross-compatibility between wasm and gopherjs builds #26

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

Closed
wants to merge 1 commit into from
Closed
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
110 changes: 24 additions & 86 deletions conn.go
Original file line number Diff line number Diff line change
@@ -12,20 +12,13 @@ import (
"net/url"
"time"

"github.com/gopherjs/gopherjs/js"
"github.com/gopherjs/gopherwasm/js"
"github.com/gopherjs/websocket/websocketjs"
)

func beginHandlerOpen(ch chan error, removeHandlers func()) func(ev *js.Object) {
return func(ev *js.Object) {
removeHandlers()
close(ch)
}
}

// closeError allows a CloseEvent to be used as an error.
type closeError struct {
*js.Object
js.Value
Code int `js:"code"`
Reason string `js:"reason"`
WasClean bool `js:"wasClean"`
@@ -41,16 +34,6 @@ func (e *closeError) Error() string {
return fmt.Sprintf("CloseEvent: (%s) (%d) %s", cleanStmt, e.Code, e.Reason)
}

func beginHandlerClose(ch chan error, removeHandlers func()) func(ev *js.Object) {
return func(ev *js.Object) {
removeHandlers()
go func() {
ch <- &closeError{Object: ev}
close(ch)
}()
}
}

type deadlineErr struct{}

func (e *deadlineErr) Error() string { return "i/o timeout: deadline reached" }
@@ -59,9 +42,6 @@ func (e *deadlineErr) Temporary() bool { return true }

var errDeadlineReached = &deadlineErr{}

// TODO(nightexcessive): Add a Dial function that allows a deadline to be
// specified.

// Dial opens a new WebSocket connection. It will block until the connection is
// established or fails to connect.
func Dial(url string) (net.Conn, error) {
@@ -71,32 +51,21 @@ func Dial(url string) (net.Conn, error) {
}
conn := &conn{
WebSocket: ws,
ch: make(chan *messageEvent, 1),
ch: make(chan []byte, 1),
}
conn.initialize()

openCh := make(chan error, 1)
// We need this so that received binary data is in ArrayBufferView format so
// that it can easily be read.
conn.SetBinaryType("arraybuffer")

var (
openHandler func(ev *js.Object)
closeHandler func(ev *js.Object)
)

// Handlers need to be removed to prevent a panic when the WebSocket closes
// immediately and fires both open and close before they can be removed.
// This way, handlers are removed before the channel is closed.
removeHandlers := func() {
ws.RemoveEventListener("open", false, openHandler)
ws.RemoveEventListener("close", false, closeHandler)
}
conn.OnMessage(conn.onMessage)
conn.OnClose(conn.onClose)

// We have to use variables for the functions so that we can remove the
// event handlers afterwards.
openHandler = beginHandlerOpen(openCh, removeHandlers)
closeHandler = beginHandlerClose(openCh, removeHandlers)
openCh := make(chan error, 1)

ws.AddEventListener("open", false, openHandler)
ws.AddEventListener("close", false, closeHandler)
conn.OnOpen(func() {
close(openCh)
})
//ws.Call("addEventListener", "close", closeHandler, false)

err, ok := <-openCh
if ok && err != nil {
@@ -110,50 +79,30 @@ func Dial(url string) (net.Conn, error) {
type conn struct {
*websocketjs.WebSocket

ch chan *messageEvent
ch chan []byte
readBuf *bytes.Reader

readDeadline time.Time
}

type messageEvent struct {
*js.Object
Data *js.Object `js:"data"`
}

func (c *conn) onMessage(event *js.Object) {
go func() {
c.ch <- &messageEvent{Object: event}
}()
func (c *conn) onMessage(data []byte) {
c.ch <- data
}

func (c *conn) onClose(event *js.Object) {
go func() {
// We queue nil to the end so that any messages received prior to
// closing get handled first.
c.ch <- nil
}()
}

// initialize adds all of the event handlers necessary for a conn to function.
// It should never be called more than once and is already called if Dial was
// used to create the conn.
func (c *conn) initialize() {
// We need this so that received binary data is in ArrayBufferView format so
// that it can easily be read.
c.BinaryType = "arraybuffer"

c.AddEventListener("message", false, c.onMessage)
c.AddEventListener("close", false, c.onClose)
func (c *conn) onClose() {
// We queue nil to the end so that any messages received prior to
// closing get handled first.
c.ch <- nil
}

// handleFrame handles a single frame received from the channel. This is a
// convenience funciton to dedupe code for the multiple deadline cases.
func (c *conn) handleFrame(message *messageEvent, ok bool) (*messageEvent, error) {
func (c *conn) handleFrame(message []byte, ok bool) ([]byte, error) {
if !ok { // The channel has been closed
return nil, io.EOF
} else if message == nil {
// See onClose for the explanation about sending a nil item.
c.Release()
close(c.ch)
return nil, io.EOF
}
@@ -163,7 +112,7 @@ func (c *conn) handleFrame(message *messageEvent, ok bool) (*messageEvent, error

// receiveFrame receives one full frame from the WebSocket. It blocks until the
// frame is received.
func (c *conn) receiveFrame(observeDeadline bool) (*messageEvent, error) {
func (c *conn) receiveFrame(observeDeadline bool) ([]byte, error) {
var deadlineChan <-chan time.Time // Receiving on a nil channel always blocks indefinitely

if observeDeadline && !c.readDeadline.IsZero() {
@@ -191,15 +140,6 @@ func (c *conn) receiveFrame(observeDeadline bool) (*messageEvent, error) {
}
}

func getFrameData(obj *js.Object) []byte {
// Check if it's an array buffer. If so, convert it to a Go byte slice.
if constructor := obj.Get("constructor"); constructor == js.Global.Get("ArrayBuffer") {
uint8Array := js.Global.Get("Uint8Array").New(obj)
return uint8Array.Interface().([]byte)
}
return []byte(obj.String())
}

func (c *conn) Read(b []byte) (n int, err error) {
if c.readBuf != nil {
n, err = c.readBuf.Read(b)
@@ -216,13 +156,11 @@ func (c *conn) Read(b []byte) (n int, err error) {
}
}

frame, err := c.receiveFrame(true)
receivedBytes, err := c.receiveFrame(true)
if err != nil {
return 0, err
}

receivedBytes := getFrameData(frame.Data)

n = copy(b, receivedBytes)
// Fast path: The entire frame's contents have been copied into b.
if n >= len(receivedBytes) {
79 changes: 58 additions & 21 deletions websocketjs/websocketjs.go
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ such as adding event listeners with callbacks.
// handle error
}
onOpen := func(ev *js.Object) {
onOpen := func(ev js.Value) {
err := ws.Send([]byte("Hello!")) // Send a binary frame.
// ...
err := ws.Send("Hello!") // Send a text frame.
@@ -30,7 +30,7 @@ such as adding event listeners with callbacks.
*/
package websocketjs

import "github.com/gopherjs/gopherjs/js"
import "github.com/gopherjs/gopherwasm/js"

// ReadyState represents the state that a WebSocket is in. For more information
// about the available states, see
@@ -81,10 +81,10 @@ func New(url string) (ws *WebSocket, err error) {
}
}()

object := js.Global.Get("WebSocket").New(url)
object := js.Global().Get("WebSocket").New(url)

ws = &WebSocket{
Object: object,
v: object,
}

return
@@ -94,7 +94,11 @@ func New(url string) (ws *WebSocket, err error) {
// object. For more information, see
// http://dev.w3.org/html5/websockets/#the-websocket-interface
type WebSocket struct {
*js.Object
v js.Value

onMessageCallback js.Callback
onOpenCallback js.Callback
onCloseCallback js.Callback

URL string `js:"url"`

@@ -105,31 +109,62 @@ type WebSocket struct {
// networking
Extensions string `js:"extensions"`
Protocol string `js:"protocol"`
}

func (ws *WebSocket) Release() {
ws.onMessageCallback.Release()
ws.onOpenCallback.Release()
ws.onCloseCallback.Release()
}

// messaging
BinaryType string `js:"binaryType"`
// SetBinaryType provides the ability to set what format
// websocket frames are in, possible values are:
// "arraybuffer"
func (ws *WebSocket) SetBinaryType(value string) {
ws.v.Set("binaryType", value)
}

// AddEventListener provides the ability to bind callback
// functions to the following available events:
// open, error, close, message
func (ws *WebSocket) AddEventListener(typ string, useCapture bool, listener func(*js.Object)) {
ws.Call("addEventListener", typ, listener, useCapture)
func (ws *WebSocket) BinaryType() string {
return ws.v.Get("binaryType").String()
}

// RemoveEventListener removes a previously bound callback function
func (ws *WebSocket) RemoveEventListener(typ string, useCapture bool, listener func(*js.Object)) {
ws.Call("removeEventListener", typ, listener, useCapture)
func (ws *WebSocket) OnMessage(callback func(value []byte)) {
ws.onMessageCallback = js.NewCallback(func(ev []js.Value) {
go func() {
// Convert event.Data to []byte
var value []byte
data := ev[0].Get("data")
uint8Array := js.Global().Get("Uint8Array").New(data)
value = make([]byte, uint8Array.Get("byteLength").Int())
a := js.TypedArrayOf(value)
a.Call("set", uint8Array)
a.Release()

callback(value)
}()
})
ws.v.Call("addEventListener", "message", ws.onMessageCallback, false)
}

// BUG(nightexcessive): When WebSocket.Send is called on a closed WebSocket, the
// thrown error doesn't seem to be caught by recover.
func (ws *WebSocket) OnClose(callback func()) {
ws.onCloseCallback = js.NewCallback(func(ev []js.Value) {
callback()
})
ws.v.Call("addEventListener", "close", ws.onCloseCallback, false)
}

func (ws *WebSocket) OnOpen(callback func()) {
ws.onOpenCallback = js.NewCallback(func(ev []js.Value) {
callback()
})
ws.v.Call("addEventListener", "open", ws.onOpenCallback, false)
}

// Send sends a message on the WebSocket. The data argument can be a string or a
// *js.Object fulfilling the ArrayBufferView definition.
// js.Value fulfilling the ArrayBufferView definition.
//
// See: http://dev.w3.org/html5/websockets/#dom-websocket-send
func (ws *WebSocket) Send(data interface{}) (err error) {
func (ws *WebSocket) Send(data []byte) (err error) {
defer func() {
e := recover()
if e == nil {
@@ -141,7 +176,9 @@ func (ws *WebSocket) Send(data interface{}) (err error) {
panic(e)
}
}()
ws.Object.Call("send", data)
a := js.TypedArrayOf(data)
ws.v.Call("send", a)
a.Release()
return
}

@@ -164,7 +201,7 @@ func (ws *WebSocket) Close() (err error) {
// Use close code closeNormalClosure to indicate that the purpose
// for which the connection was established has been fulfilled.
// See https://tools.ietf.org/html/rfc6455#section-7.4.
ws.Object.Call("close", closeNormalClosure)
ws.v.Call("close", closeNormalClosure)
return
}