diff --git a/conn.go b/conn.go index 8b9486c..feabb8d 100644 --- a/conn.go +++ b/conn.go @@ -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) { diff --git a/websocketjs/websocketjs.go b/websocketjs/websocketjs.go index 68acfcb..8adefd1 100644 --- a/websocketjs/websocketjs.go +++ b/websocketjs/websocketjs.go @@ -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 }