// SPDX-License-Identifier: 0BSD // Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io //go:build js && wasm // +build js,wasm package interfaces import ( "fmt" "net" "syscall/js" "time" "git.quad4.io/Networks/Reticulum-Go/pkg/common" "git.quad4.io/Networks/Reticulum-Go/pkg/debug" ) const ( WS_MTU = 1064 WS_BITRATE = 10000000 WS_RECONNECT_DELAY = 2 * time.Second ) type WebSocketInterface struct { BaseInterface wsURL string ws js.Value connected bool messageQueue [][]byte } func NewWebSocketInterface(name string, wsURL string, enabled bool) (*WebSocketInterface, error) { ws := &WebSocketInterface{ BaseInterface: NewBaseInterface(name, common.IF_TYPE_UDP, enabled), wsURL: wsURL, messageQueue: make([][]byte, 0), } ws.MTU = WS_MTU ws.Bitrate = WS_BITRATE return ws, nil } func (wsi *WebSocketInterface) GetName() string { return wsi.Name } func (wsi *WebSocketInterface) GetType() common.InterfaceType { return wsi.Type } func (wsi *WebSocketInterface) GetMode() common.InterfaceMode { return wsi.Mode } func (wsi *WebSocketInterface) IsOnline() bool { wsi.Mutex.RLock() defer wsi.Mutex.RUnlock() return wsi.Online && wsi.connected } func (wsi *WebSocketInterface) IsDetached() bool { wsi.Mutex.RLock() defer wsi.Mutex.RUnlock() return wsi.Detached } func (wsi *WebSocketInterface) Detach() { wsi.Mutex.Lock() defer wsi.Mutex.Unlock() wsi.Detached = true wsi.Online = false wsi.closeWebSocket() } func (wsi *WebSocketInterface) Enable() { wsi.Mutex.Lock() defer wsi.Mutex.Unlock() wsi.Enabled = true } func (wsi *WebSocketInterface) Disable() { wsi.Mutex.Lock() defer wsi.Mutex.Unlock() wsi.Enabled = false wsi.closeWebSocket() } func (wsi *WebSocketInterface) Start() error { wsi.Mutex.Lock() defer wsi.Mutex.Unlock() if wsi.ws.Truthy() { return fmt.Errorf("WebSocket already started") } ws := js.Global().Get("WebSocket").New(wsi.wsURL) ws.Set("binaryType", "arraybuffer") ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) interface{} { wsi.Mutex.Lock() wsi.connected = true wsi.Online = true wsi.Mutex.Unlock() debug.Log(debug.DEBUG_INFO, "WebSocket connected", "name", wsi.Name, "url", wsi.wsURL) wsi.Mutex.Lock() queue := make([][]byte, len(wsi.messageQueue)) copy(queue, wsi.messageQueue) wsi.messageQueue = wsi.messageQueue[:0] wsi.Mutex.Unlock() for _, msg := range queue { wsi.sendWebSocketMessage(msg) } return nil })) ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} { if len(args) < 1 { return nil } event := args[0] data := event.Get("data") var packet []byte if data.Type() == js.TypeString { packet = []byte(data.String()) } else if data.Type() == js.TypeObject { array := js.Global().Get("Uint8Array").New(data) length := array.Get("length").Int() packet = make([]byte, length) js.CopyBytesToGo(packet, array) } else { debug.Log(debug.DEBUG_ERROR, "Unknown WebSocket message type", "type", data.Type().String()) return nil } if len(packet) < 1 { debug.Log(debug.DEBUG_ERROR, "WebSocket message empty") return nil } wsi.Mutex.Lock() wsi.RxBytes += uint64(len(packet)) wsi.Mutex.Unlock() wsi.ProcessIncoming(packet) return nil })) ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} { debug.Log(debug.DEBUG_ERROR, "WebSocket error", "name", wsi.Name) return nil })) ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) interface{} { wsi.Mutex.Lock() wsi.connected = false wsi.Online = false wsi.Mutex.Unlock() debug.Log(debug.DEBUG_INFO, "WebSocket closed", "name", wsi.Name) if wsi.Enabled && !wsi.Detached { time.Sleep(WS_RECONNECT_DELAY) go wsi.Start() } return nil })) wsi.ws = ws return nil } func (wsi *WebSocketInterface) Stop() error { wsi.Mutex.Lock() defer wsi.Mutex.Unlock() wsi.Enabled = false wsi.closeWebSocket() return nil } func (wsi *WebSocketInterface) closeWebSocket() { if wsi.ws.Truthy() { wsi.ws.Call("close") wsi.ws = js.Value{} } wsi.connected = false wsi.Online = false } func (wsi *WebSocketInterface) Send(data []byte, addr string) error { if !wsi.IsEnabled() { return fmt.Errorf("interface not enabled") } wsi.Mutex.Lock() wsi.TxBytes += uint64(len(data)) wsi.Mutex.Unlock() if !wsi.connected { wsi.Mutex.Lock() wsi.messageQueue = append(wsi.messageQueue, data) wsi.Mutex.Unlock() return nil } return wsi.sendWebSocketMessage(data) } func (wsi *WebSocketInterface) sendWebSocketMessage(data []byte) error { if !wsi.ws.Truthy() { return fmt.Errorf("WebSocket not initialized") } if wsi.ws.Get("readyState").Int() != 1 { return fmt.Errorf("WebSocket not open") } array := js.Global().Get("Uint8Array").New(len(data)) js.CopyBytesToJS(array, data) wsi.ws.Call("send", array) debug.Log(debug.DEBUG_VERBOSE, "WebSocket sent packet", "name", wsi.Name, "bytes", len(data)) return nil } func (wsi *WebSocketInterface) ProcessOutgoing(data []byte) error { return wsi.Send(data, "") } func (wsi *WebSocketInterface) GetConn() net.Conn { return nil } func (wsi *WebSocketInterface) GetMTU() int { return wsi.MTU } func (wsi *WebSocketInterface) IsEnabled() bool { wsi.Mutex.RLock() defer wsi.Mutex.RUnlock() return wsi.Enabled && wsi.Online && !wsi.Detached }