Skip to content

Commit

Permalink
Merge pull request #9 from tarndt/ta-betterdocs
Browse files Browse the repository at this point in the history
Improve documentation.
  • Loading branch information
tarndt authored Dec 26, 2019
2 parents 500d015 + 450200a commit cf2b2a3
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 11 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# wasmws: Webassembly Websocket (for Go)
# wasmws: Web-Assembly Websocket (for Go)

## What is wasms? Why would I want to use this?
## What is wasmws? Why would I want to use this?

[wasmws](https://github.com/tarndt/wasmws) was written primarily to allow [Go](https://golang.org/) applications targeting [WASM](https://en.wikipedia.org/wiki/WebAssembly) to communicate with a [gRPC](https://grpc.io/) server. This is normally challenging for two reasons:

Expand Down Expand Up @@ -69,7 +69,7 @@ If you do not have Go installed, you will of course need to [install it](https:/
## Alternatives

1. Use [gRPC-Web](https://github.com/grpc/grpc-web) as a HTTP to gRPC gateway/proxy. (If you don't mind a TCP connection per request, running extra middleware which are also extra points of failure...)
2. Use "[nhooyr.io/websocket](https://github.com/nhooyr/websocket)"'s implemtation which unlike "wasmws" does not use the browser provided websocket functionality. Test and bench your own use-case!
2. Use "[nhooyr.io/websocket](https://github.com/nhooyr/websocket)"'s implementation which unlike "wasmws" does not use the browser provided websocket functionality. Test and bench your own use-case!

## Future

Expand All @@ -81,6 +81,6 @@ wasmws is actively being maintained, but that does not mean there are not things

## Contributing

[Issues](https://github.com/tarndt/wasmws/issues), and espcially issues with [pull requests](https://github.com/tarndt/wasmws/pulls) are welcome!
[Issues](https://github.com/tarndt/wasmws/issues), and especially issues with [pull requests](https://github.com/tarndt/wasmws/pulls) are welcome!

This code is licensed under [MPL 2.0](https://en.wikipedia.org/wiki/Mozilla_Public_License).
9 changes: 9 additions & 0 deletions arrayreader_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,30 @@ var arrayReaderPool = sync.Pool{
},
}

//newReaderArrayPromise returns a arrayReader from a JavaScript promise for
// an array buffer: See https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer
func newReaderArrayPromise(arrayPromise js.Value) *arrayReader {
ar := arrayReaderPool.Get().(*arrayReader)
ar.jsPromise = arrayPromise
return ar
}

//newReaderArrayPromise returns a arrayReader from a JavaScript array buffer:
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
func newReaderArrayBuffer(arrayBuffer js.Value) (*arrayReader, int) {
ar := arrayReaderPool.Get().(*arrayReader)
ar.remaining, ar.read = ar.fromArray(arrayBuffer), true
return ar, len(ar.remaining)
}

//Close closes the arrayReader and returns it to a pool. DO NOT USE FURTHER!
func (ar *arrayReader) Close() error {
ar.Reset()
arrayReaderPool.Put(ar)
return nil
}

//Reset makes this arrayReader ready for reuse
func (ar *arrayReader) Reset() {
const bufMax = socketStreamThresholdBytes
ar.jsPromise, ar.read, ar.err = js.Value{}, false, nil
Expand All @@ -51,6 +57,7 @@ func (ar *arrayReader) Reset() {
}
}

//Read implements the standard io.Reader interface
func (ar *arrayReader) Read(buf []byte) (n int, err error) {
if ar.err != nil {
return 0, ar.err
Expand Down Expand Up @@ -89,6 +96,8 @@ func (ar *arrayReader) Read(buf []byte) (n int, err error) {
return n, nil
}

//fromArray is a helper that that copies a JavaScript ArrayBuffer into go-space
// and uses an existing go buffer if possible.
func (ar *arrayReader) fromArray(arrayBuffer js.Value) []byte {
jsBuf := uint8Array.New(arrayBuffer)
count := jsBuf.Get("byteLength").Int()
Expand Down
10 changes: 10 additions & 0 deletions dial_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import (
"strings"
)

//Dial is a standard legacy network dialer that returns a websocket-based connection.
//See: DialContext for details on the network and address.
func Dial(network, address string) (net.Conn, error) {
return DialContext(context.Background(), network, address)
}

//DialContext is a standard context-aware network dialer that returns a websocket-based connection.
// The address is a URL that should be in the form of "ws://host/path..." for unsecured websockets
// and "wss://host/path..." for secured websockets. If tunnel a TLS based protocol
// over a "wss://..." websocket you will get TLS twice, once on the websocket using
// the browsers TLS stack and another using the Go (or other compiled) TLS stack.
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if network != "websocket" {
return nil, fmt.Errorf("Invalid network: %q; Details: Only \"websocket\" network is supported", network)
Expand All @@ -22,6 +29,9 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
return New(ctx, address)
}

//GRPCDialer is a helper that can be used with grpc.WithContextDialer to call DialContext.
//The address provided to the calling grpc.Dial should be in the form "passthrough:///"+websocketURL
// where websocketURL matches the description in DialContext.
func GRPCDialer(ctx context.Context, address string) (net.Conn, error) {
return DialContext(ctx, "websocket", address)
}
9 changes: 9 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package wasmws

/*
Package wasm is a library allows you to use a websocket as net.Conn for arbitrary traffic (this includes tunneling protocols like HTTP, gRPC or any other TCP protocol over it). This is most useful for protocols that are not normally exposed to client side web applications. The specific motivation of this package was to allow Go applications targeting WASM to communicate with a gRPC server.
wasmws.WebSocket is the provided net.Conn implementation intended to be used from within Go WASM applications and wasmws.WebSockListener is the provided net.Listener intended to be used from server side native Go applications to accept client connections.
For extended details and examples please see: https://github.com/tarndt/wasmws/blob/master/README.md
*/
4 changes: 4 additions & 0 deletions helpertypes_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var (
uint8Array = js.Global().Get("Uint8Array")
)

//timeoutErr is a net.Addr implementation for the websocket to use when fufilling
// the net.Conn interface
type timeoutError struct{}

func (timeoutError) Error() string { return "deadline exceeded" }
Expand All @@ -17,6 +19,8 @@ func (timeoutError) Timeout() bool { return true }

func (timeoutError) Temporary() bool { return true }

//wsAddr is a net.Addr implementation for the websocket to use when fufilling
// the net.Conn interface
type wsAddr string

func (wsAddr) Network() string { return "websocket" }
Expand Down
7 changes: 7 additions & 0 deletions sockettype_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ const (

type socketType uint8

//newSocketType returns the socket type of the provided JavaScript websocket object
func newSocketType(websocket js.Value) socketType {
return newSocketTypeString(websocket.Get("binaryType").String())
}

//newSocketTypeString returns a socketType from a string of the socket type that
// matches the JavaScript websock.binaryType property:
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType
func newSocketTypeString(wsTypeStr string) socketType {
switch wsTypeStr {
case "blob":
Expand All @@ -27,6 +31,8 @@ func newSocketTypeString(wsTypeStr string) socketType {
}
}

//String returns the a string of the socket type that matches the JavaScript
// websock.binaryType property: See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType
func (st socketType) String() string {
switch st {
case socketTypeArrayBuffer:
Expand All @@ -38,6 +44,7 @@ func (st socketType) String() string {
}
}

//Set sets the type of the provided JavaScript websocket to itself
func (st socketType) Set(websocket js.Value) {
websocket.Set("binaryType", st.String())
if debugVerbose {
Expand Down
5 changes: 5 additions & 0 deletions streamreader_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@ var streamReaderPool = sync.Pool{
},
}

//newStreamReaderPromise returns a streamReader from a JavaScript promise for
// a stream reader: See https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream
func newStreamReaderPromise(streamPromise js.Value) *streamReader {
sr := streamReaderPool.Get().(*streamReader)
sr.jsPromise = streamPromise
return sr
}

//Close closes the streamReader and returns it to a pool. DO NOT USE FURTHER!
func (sr *streamReader) Close() error {
sr.Reset()
streamReaderPool.Put(sr)
return nil
}

//Reset makes this streamReader ready for reuse
func (sr *streamReader) Reset() {
const bufMax = socketStreamThresholdBytes
sr.jsPromise, sr.err = js.Value{}, nil
Expand All @@ -43,6 +47,7 @@ func (sr *streamReader) Reset() {
}
}

//Read implements the standard io.Reader interface
func (sr *streamReader) Read(p []byte) (n int, err error) {
if sr.err != nil {
return 0, sr.err
Expand Down
62 changes: 55 additions & 7 deletions websock_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ import (
)

const (
socketStreamThresholdBytes = 1024
debugVerbose = false
socketStreamThresholdBytes = 1024 //If enabled, the Blob interface will be used when consecutive messages exceed this threshold
debugVerbose = false //Set to true if you are debugging issues, this gates many prints that would kill performance
)

var (
//EnableBlobStreaming allows the browser provided Websocket's streaming
// interface to be used, if supported.
EnableBlobStreaming bool = true
blobSupported bool
ErrWebsocketClosed = errors.New("WebSocket: Web socket is closed")

//ErrWebsocketClosed is returned when operations are performed on a closed Websocket
ErrWebsocketClosed = errors.New("WebSocket: Web socket is closed")

blobSupported bool //set to true by init if browser supports the Blob interface
)

//init checks to see if the browser hosting this application support the Websocket Blob interface
func init() {
newBlob := js.Global().Get("Blob")
if newBlob == jsUndefined {
Expand All @@ -37,6 +43,7 @@ func init() {
}
}

//WebSocket is a Go struct that wraps the web browser's JavaScript websocket object and provides a net.Conn interface
type WebSocket struct {
ctx context.Context
ctxCancel context.CancelFunc
Expand All @@ -63,6 +70,11 @@ type WebSocket struct {
cleanup []func()
}

//New returns a new WebSocket using the provided dial context and websocket URL.
// The URL should be in the form of "ws://host/path..." for unsecured websockets
// and "wss://host/path..." for secured websockets. If tunnel a TLS based protocol
// over a "wss://..." websocket you will get TLS twice, once on the websocket using
// the browsers TLS stack and another using the Go (or other compiled) TLS stack.
func New(dialCtx context.Context, URL string) (*WebSocket, error) {
ctx, cancel := context.WithCancel(context.Background())
ws := &WebSocket{
Expand Down Expand Up @@ -104,6 +116,19 @@ func New(dialCtx context.Context, URL string) (*WebSocket, error) {
for _, cleanup := range ws.cleanup {
cleanup()
}

for {
select {
case pending := <-ws.readCh:
if closer, hasClose := pending.(io.Closer); hasClose {
closer.Close()
}
continue

default:
}
break
}
}()

//Wait for connection or failure
Expand Down Expand Up @@ -136,6 +161,7 @@ func New(dialCtx context.Context, URL string) (*WebSocket, error) {
return ws, nil
}

//Close shuts the websocket down
func (ws *WebSocket) Close() error {
if debugVerbose {
println("Websocket: Internal close")
Expand All @@ -144,14 +170,19 @@ func (ws *WebSocket) Close() error {
return nil
}

//LocalAddr returns a dummy websocket address to satisfy net.Conn, see: wsAddr
func (ws *WebSocket) LocalAddr() net.Addr {
return wsAddr(ws.URL)
}

//RemoteAddr returns a dummy websocket address to satisfy net.Conn, see: wsAddr
func (ws *WebSocket) RemoteAddr() net.Addr {
return wsAddr(ws.URL)
}

//Write implements the standard io.Writer interface. Due to the JavaScript writes
// being internally buffered it will never block and a write timeout from a
// previous write may not surface until a subsequent write.
func (ws *WebSocket) Write(buf []byte) (n int, err error) {
//Check for noop
writeCount := len(buf)
Expand Down Expand Up @@ -190,7 +221,7 @@ func (ws *WebSocket) Write(buf []byte) (n int, err error) {
ws.setDeadline(ws.writeDeadlineTimer, newWriteDeadline)

case <-ws.writeDeadlineTimer.C:
if reamining := ws.ws.Get("bufferedAmount").Int(); reamining > 0 {
if remaining := ws.ws.Get("bufferedAmount").Int(); remaining > 0 {
return 0, timeoutError{}
}

Expand Down Expand Up @@ -222,6 +253,7 @@ func (ws *WebSocket) Write(buf []byte) (n int, err error) {
return writeCount, nil
}

//Read implements the standard io.Reader interface (typical semantics)
func (ws *WebSocket) Read(buf []byte) (int, error) {
//Check for noop
if len(buf) < 1 {
Expand Down Expand Up @@ -303,6 +335,7 @@ func (ws *WebSocket) SetDeadline(future time.Time) (err error) {
return nil
}

//SetWriteDeadline implements the Conn SetWriteDeadline method
func (ws *WebSocket) SetWriteDeadline(future time.Time) error {
if debugVerbose {
println("Websocket: Set write deadline for", future.String())
Expand All @@ -311,6 +344,7 @@ func (ws *WebSocket) SetWriteDeadline(future time.Time) error {
return nil
}

//SetReadDeadline implements the Conn SetReadDeadline method
func (ws *WebSocket) SetReadDeadline(future time.Time) error {
if debugVerbose {
println("Websocket: Set read deadline for", future.String())
Expand All @@ -319,7 +353,7 @@ func (ws *WebSocket) SetReadDeadline(future time.Time) error {
return nil
}

//Only call from New or Read!
//setDeadline is used internally; Only call from New or Read!
func (ws *WebSocket) setDeadline(timer *time.Timer, future time.Time) error {
if !timer.Stop() {
select {
Expand All @@ -333,6 +367,7 @@ func (ws *WebSocket) setDeadline(timer *time.Timer, future time.Time) error {
return nil
}

//addHandler is used internall by the WebSocket constructor
func (ws *WebSocket) addHandler(handler func(this js.Value, args []js.Value), event string) {
jsHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
handler(this, args)
Expand All @@ -346,20 +381,26 @@ func (ws *WebSocket) addHandler(handler func(this js.Value, args []js.Value), ev
ws.cleanup = append(ws.cleanup, cleanup)
}

//handleOpen is a callback for JavaScript to notify Go when the websocket is open:
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onopen
func (ws *WebSocket) handleOpen(_ js.Value, _ []js.Value) {
if debugVerbose {
println("Websocket: Open JS callback!")
}
close(ws.openCh)
}

//handleClose is a callback for JavaScript to notify Go when the websocket is closed:
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onclose
func (ws *WebSocket) handleClose(_ js.Value, _ []js.Value) {
if debugVerbose {
println("Websocket: Close JS callback!")
}
ws.ctxCancel()
}

//handleError is a callback for JavaScript to notify Go when the websocket is in an error state:
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onerror
func (ws *WebSocket) handleError(_ js.Value, args []js.Value) {
if debugVerbose {
println("Websocket: Error JS Callback")
Expand All @@ -375,11 +416,18 @@ func (ws *WebSocket) handleError(_ js.Value, args []js.Value) {
}
}

//handleMessage is a callback for JavaScript to notify Go when the websocket has a new message:
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onmessage
func (ws *WebSocket) handleMessage(_ js.Value, args []js.Value) {
if debugVerbose {
println("Websocket: New Message JS Callback")
}

select {
case <-ws.ctx.Done():
default:
}

var rdr io.Reader
var size int

Expand Down Expand Up @@ -408,7 +456,7 @@ func (ws *WebSocket) handleMessage(_ js.Value, args []js.Value) {
}

select {
case ws.readCh <- rdr:
case ws.readCh <- rdr: //Try non-blocking queue first...
if debugVerbose {
println("Websocket: JS read callback sync enqueue")
}
Expand Down
Loading

0 comments on commit cf2b2a3

Please sign in to comment.