diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index 76faa4924..603d099ca 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -16,6 +16,7 @@ import ( "crypto/x509" "encoding/hex" "encoding/json" + "errors" "fmt" "log" "math/rand/v2" @@ -589,11 +590,29 @@ func (i *jsIPN) dial(network, addr string) js.Value { func (i *jsIPN) listen(network, addr string) js.Value { return makePromise(func() (any, error) { - pc, err := i.ns.ListenPacket(network, addr) - if err != nil { - return nil, err + switch network { + case "tcp", "tcp4", "tcp6": + // netstack.ListenTCP only accepts tcp4/tcp6; bare "tcp" + // defaults to IPv4 to match net.Listen's typical behavior + // when given an unspecified address. + n := network + if n == "tcp" { + n = "tcp4" + } + ln, err := i.ns.ListenTCP(n, addr) + if err != nil { + return nil, err + } + return wrapTCPListener(ln), nil + case "udp", "udp4", "udp6": + pc, err := i.ns.ListenPacket(network, addr) + if err != nil { + return nil, err + } + return wrapPacketConn(pc), nil + default: + return nil, fmt.Errorf("unsupported network %q", network) } - return wrapPacketConn(pc), nil }) } @@ -711,6 +730,54 @@ func wrapConn(conn net.Conn) map[string]any { } } +// wrapTCPListener exposes a net.Listener to JavaScript as an object with +// accept/close/addr methods plus a Symbol.asyncIterator implementation, so +// callers can write `for await (const conn of listener)`. +func wrapTCPListener(ln net.Listener) js.Value { + obj := js.Global().Get("Object").New() + obj.Set("accept", js.FuncOf(func(this js.Value, args []js.Value) any { + return makePromise(func() (any, error) { + conn, err := ln.Accept() + if err != nil { + return nil, err + } + return wrapConn(conn), nil + }) + })) + obj.Set("close", js.FuncOf(func(this js.Value, args []js.Value) any { + return ln.Close() != nil + })) + obj.Set("addr", js.FuncOf(func(this js.Value, args []js.Value) any { + return ln.Addr().String() + })) + + asyncIterSym := js.Global().Get("Symbol").Get("asyncIterator") + iterFactory := js.FuncOf(func(this js.Value, args []js.Value) any { + iter := js.Global().Get("Object").New() + iter.Set("next", js.FuncOf(func(this js.Value, args []js.Value) any { + return makePromise(func() (any, error) { + conn, err := ln.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + return map[string]any{ + "value": js.Undefined(), + "done": true, + }, nil + } + return nil, err + } + return map[string]any{ + "value": wrapConn(conn), + "done": false, + }, nil + }) + })) + return iter + }) + js.Global().Get("Reflect").Call("set", obj, asyncIterSym, iterFactory) + return obj +} + // wrapPacketConn exposes a net.PacketConn to JavaScript with binary (Uint8Array) I/O. func wrapPacketConn(pc net.PacketConn) map[string]any { return map[string]any{