//go:build js && wasm package jsws import ( "errors" "fmt" "sync" "syscall/js" "time" ) // maxQueuedMessages bounds the receive queue. A relay message is small; a // client that stops reading while a bridge streams filters is bounded here // rather than by growing the wasm heap without limit. const maxQueuedMessages = 4096 // Conn is a browser WebSocket implementing relay.Transport. type Conn struct { ws js.Value mu sync.Mutex queue [][]byte closed bool err error notify chan struct{} funcs []js.Func } // Dial opens a WebSocket to a ws:// or wss:// URL using the browser's // WebSocket implementation. func Dial(rawURL string) (*Conn, error) { ctor := js.Global().Get("WebSocket") if ctor.Type() == js.TypeUndefined { return nil, errors.New("jsws: WebSocket is not available") } c := &Conn{notify: make(chan struct{}, 1)} opened := make(chan struct{}) failed := make(chan error, 1) openOnce := sync.Once{} openedSignal := func() { openOnce.Do(func() { close(opened) }) } ws, err := newWebSocket(ctor, rawURL) if err != nil { return nil, err } c.ws = ws onOpen := js.FuncOf(func(this js.Value, args []js.Value) any { openedSignal() return nil }) onMessage := js.FuncOf(func(this js.Value, args []js.Value) any { if len(args) == 0 { return nil } data := args[0].Get("data") if data.Type() != js.TypeString { return nil } c.push([]byte(data.String())) return nil }) onClose := js.FuncOf(func(this js.Value, args []js.Value) any { err := errors.New("jsws: connection closed") c.fail(err) select { case failed <- err: default: } return nil }) onError := js.FuncOf(func(this js.Value, args []js.Value) any { err := errors.New("jsws: websocket error") c.fail(err) select { case failed <- err: default: } return nil }) c.funcs = []js.Func{onOpen, onMessage, onClose, onError} ws.Set("onopen", onOpen) ws.Set("onmessage", onMessage) ws.Set("onclose", onClose) ws.Set("onerror", onError) select { case <-opened: return c, nil case err := <-failed: return nil, err case <-time.After(15 * time.Second): func() { defer func() { recover() }() ws.Call("close") }() return nil, errors.New("jsws: connect timeout") } } func newWebSocket(ctor js.Value, rawURL string) (ws js.Value, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("jsws: websocket: %v", r) } }() return ctor.New(rawURL), nil } func (c *Conn) push(msg []byte) { c.mu.Lock() if c.closed { c.mu.Unlock() return } if len(c.queue) >= maxQueuedMessages { c.err = errors.New("jsws: receive queue overflow") c.closed = true c.mu.Unlock() select { case c.notify <- struct{}{}: default: } return } c.queue = append(c.queue, msg) c.mu.Unlock() select { case c.notify <- struct{}{}: default: } } func (c *Conn) fail(err error) { c.mu.Lock() if !c.closed { c.closed = true c.err = err } c.mu.Unlock() select { case c.notify <- struct{}{}: default: } } // ReadMessage returns the next text message. func (c *Conn) ReadMessage() ([]byte, error) { for { c.mu.Lock() if len(c.queue) > 0 { msg := c.queue[0] c.queue = c.queue[1:] c.mu.Unlock() return msg, nil } if c.closed { err := c.err c.mu.Unlock() if err == nil { err = errors.New("jsws: closed") } return nil, err } c.mu.Unlock() <-c.notify } } // WriteText sends one text message. func (c *Conn) WriteText(payload []byte) (err error) { c.mu.Lock() closed := c.closed c.mu.Unlock() if closed { return errors.New("jsws: closed") } defer func() { if r := recover(); r != nil { err = fmt.Errorf("jsws: send: %v", r) } }() c.ws.Call("send", string(payload)) return nil } // Close closes the WebSocket. func (c *Conn) Close() error { c.mu.Lock() already := c.closed c.closed = true c.mu.Unlock() if !already { func() { defer func() { recover() }() c.ws.Call("close") }() } select { case c.notify <- struct{}{}: default: } return nil }