Artifact ce53d72b81e8fb1156ccc63685d33f52a81ec9976f9bff0ec23858a95117b8ee:
- File
leaf/wsdialer/conn.go
— part of check-in
[eda7960b81]
at
2026-04-23 19:23:42
on branch trunk
— rename module to github.com/danmestas/EdgeSync
Fix the case-sensitive module path to match the actual GitHub repo URL (danmestas/EdgeSync). Remove tracked leaf/leaf binary and ignore .claude/skills/ scratchpad dirs. (user: [email protected], size: 3620) [annotate] [blame] [check-ins using] [more...]
//go:build js package wsdialer import ( "bytes" "errors" "fmt" "net" "sync" "syscall/js" "time" ) // wsConn wraps a browser WebSocket as a net.Conn. // Messages from the WebSocket are buffered in a channel; Read() drains // them through a bytes.Reader to provide stream semantics. // // Note: js.FuncOf callbacks are not released — acceptable for spike scope. // Production code should store them on the struct and Release() in Close(). type wsConn struct { ws js.Value msgCh chan []byte reader *bytes.Reader mu sync.Mutex closed bool openCh chan struct{} errCh chan error } func newWSConn(url string) *wsConn { c := &wsConn{ msgCh: make(chan []byte, 256), reader: bytes.NewReader(nil), openCh: make(chan struct{}), errCh: make(chan error, 1), } ws := js.Global().Get("WebSocket").New(url) ws.Set("binaryType", "arraybuffer") c.ws = ws ws.Call("addEventListener", "open", js.FuncOf(func(_ js.Value, _ []js.Value) any { close(c.openCh) return nil })) ws.Call("addEventListener", "error", js.FuncOf(func(_ js.Value, args []js.Value) any { select { case c.errCh <- fmt.Errorf("websocket error"): default: } return nil })) ws.Call("addEventListener", "close", js.FuncOf(func(_ js.Value, _ []js.Value) any { c.mu.Lock() c.closed = true c.mu.Unlock() // Unblock any pending Read. select { case c.msgCh <- nil: default: } return nil })) ws.Call("addEventListener", "message", js.FuncOf(func(_ js.Value, args []js.Value) any { arrayBuf := args[0].Get("data") buf := make([]byte, arrayBuf.Get("byteLength").Int()) js.CopyBytesToGo(buf, js.Global().Get("Uint8Array").New(arrayBuf)) // Drop-newest if buffer full. select { case c.msgCh <- buf: default: } return nil })) return c } // waitOpen blocks until the WebSocket is open or fails. func (c *wsConn) waitOpen() error { select { case <-c.openCh: return nil case err := <-c.errCh: return err case <-time.After(5 * time.Second): return errors.New("websocket open timeout (5s)") } } // Read implements net.Conn. Uses an internal bytes.Reader to bridge // WebSocket message framing to stream semantics. func (c *wsConn) Read(p []byte) (int, error) { // Drain leftover bytes from the previous message first. if c.reader.Len() > 0 { return c.reader.Read(p) } c.mu.Lock() closed := c.closed c.mu.Unlock() if closed { return 0, net.ErrClosed } msg, ok := <-c.msgCh if !ok || msg == nil { return 0, net.ErrClosed } c.reader.Reset(msg) return c.reader.Read(p) } // Write implements net.Conn. Sends binary data via WebSocket. func (c *wsConn) Write(p []byte) (int, error) { c.mu.Lock() closed := c.closed c.mu.Unlock() if closed { return 0, net.ErrClosed } buf := js.Global().Get("Uint8Array").New(len(p)) js.CopyBytesToJS(buf, p) c.ws.Call("send", buf.Get("buffer")) return len(p), nil } // Close implements net.Conn. func (c *wsConn) Close() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { return nil } c.closed = true c.ws.Call("close") return nil } // Stubs — nats.go uses its own ping/pong timeouts, not net.Conn deadlines. func (c *wsConn) LocalAddr() net.Addr { return wsAddr{} } func (c *wsConn) RemoteAddr() net.Addr { return wsAddr{} } func (c *wsConn) SetDeadline(_ time.Time) error { return nil } func (c *wsConn) SetReadDeadline(_ time.Time) error { return nil } func (c *wsConn) SetWriteDeadline(_ time.Time) error { return nil } type wsAddr struct{} func (wsAddr) Network() string { return "websocket" } func (wsAddr) String() string { return "websocket" }