feat(tsconnect): add TCP listening to ipn.listen
Extend ipn.listen to also accept "tcp"/"tcp4"/"tcp6" and return a
TCPListener bound to a netstack gonet.TCPListener. The listener
exposes accept/close/addr like a Go net.Listener and additionally
implements Symbol.asyncIterator so JS callers can write:
for await (const conn of listener) { ... }
The async iterator returns done when the listener is closed (via
errors.Is(net.ErrClosed)) and rejects on any other accept error.
Symbol-keyed properties are set via Reflect.set since syscall/js
only exposes string-keyed Set.
This commit is contained in:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user