NewJSInterface creates a JavaScript object wrapping the WebSocket connection. It exposes: send(string|Uint8Array), close(), and callback properties onmessage, onclose, onerror. Callback properties may be set from the JS thread while the read loop goroutine reads them. In WASM this is safe because G
(conn *Conn)
| 158 | // single thread, but the design would need synchronization on |
| 159 | // multi-threaded runtimes. |
| 160 | func NewJSInterface(conn *Conn) js.Value { |
| 161 | obj := js.Global().Get("Object").Call("create", js.Null()) |
| 162 | |
| 163 | sendFunc := js.FuncOf(func(_ js.Value, args []js.Value) any { |
| 164 | if len(args) < 1 { |
| 165 | log.Errorf("websocket send requires a data argument") |
| 166 | return js.ValueOf(false) |
| 167 | } |
| 168 | |
| 169 | data := args[0] |
| 170 | switch data.Type() { |
| 171 | case js.TypeString: |
| 172 | if err := conn.WriteText([]byte(data.String())); err != nil { |
| 173 | log.Errorf("failed to send websocket text: %v", err) |
| 174 | return js.ValueOf(false) |
| 175 | } |
| 176 | default: |
| 177 | buf, err := jsToBytes(data) |
| 178 | if err != nil { |
| 179 | log.Errorf("failed to convert js value to bytes: %v", err) |
| 180 | return js.ValueOf(false) |
| 181 | } |
| 182 | if err := conn.WriteBinary(buf); err != nil { |
| 183 | log.Errorf("failed to send websocket binary: %v", err) |
| 184 | return js.ValueOf(false) |
| 185 | } |
| 186 | } |
| 187 | return js.ValueOf(true) |
| 188 | }) |
| 189 | obj.Set("send", sendFunc) |
| 190 | |
| 191 | closeFunc := js.FuncOf(func(_ js.Value, _ []js.Value) any { |
| 192 | if err := conn.Close(); err != nil { |
| 193 | log.Debugf("failed to close websocket: %v", err) |
| 194 | } |
| 195 | return js.Undefined() |
| 196 | }) |
| 197 | obj.Set("close", closeFunc) |
| 198 | |
| 199 | go func() { |
| 200 | defer func() { |
| 201 | if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { |
| 202 | log.Debugf("close websocket on readLoop exit: %v", err) |
| 203 | } |
| 204 | }() |
| 205 | readLoop(conn, obj) |
| 206 | // Undefining before Release turns post-close JS calls into TypeError |
| 207 | // instead of a silent "call to released function". |
| 208 | obj.Set("send", js.Undefined()) |
| 209 | obj.Set("close", js.Undefined()) |
| 210 | sendFunc.Release() |
| 211 | closeFunc.Release() |
| 212 | }() |
| 213 | |
| 214 | return obj |
| 215 | } |
| 216 | |
| 217 | func jsToBytes(data js.Value) ([]byte, error) { |
nothing calls this directly
no test coverage detected