runtime:net
TCP sockets. connect() follows the WinterTC Sockets API; listen() yields inbound connections. Bytes move over web streams. bind() is UDP — messages, not streams.
Capability: Net (connect, send) / NetListen (listen, bind)
Exposed as an ES module under the runtime: scheme. Status: Available.
All I/O is async
All I/O is async over web streams — nothing blocks the event loop. Closing a socket's writable half-closes (sends FIN) while reads continue.
Import
JavaScript
import { connect, listen, bind } from "runtime:net";
Functions
| Function | Type | Description | Example |
|---|---|---|---|
connect(address, options?) | (Address, { secureTransport?, sni?, alpn?, allowHalfOpen? }) => Socket | Open an outbound TCP or TLS connection (the WinterTC Sockets API). Returns a Socket synchronously; .opened settles once connected. address is "host:port" or { hostname, port }. secureTransport: "on" negotiates TLS, "starttls" opens plaintext for a later startTls(); sni overrides the server name (default: the host); alpn offers protocols (the negotiated one is SocketInfo.alpn); allowHalfOpen keeps writing after the peer's FIN. | const sock = connect({ hostname: "db.internal", port: 5432 }) |
listen(options) | ({ hostname?, port, secureTransport?, cert?, key?, alpn?, reusePort? }) => Listener | Bind a listening socket. hostname defaults to 0.0.0.0 (all interfaces — pass 127.0.0.1 for a loopback-only server; a locked-down host may refuse a wildcard bind). port 0 picks an ephemeral port (read it from listener.addr). Returns an async-iterable Listener of inbound Sockets. secureTransport: "on" terminates TLS on each accept — pass a PEM cert + key (and optional alpn); the cert/key are inline, so server TLS needs no capability beyond NetListen. reusePort shares the port across processes (SO_REUSEPORT, Unix only). | const server = listen({ hostname: "127.0.0.1", port: 8080 }) |
bind(options) | ({ hostname?, port, reusePort?, reuseAddress?, broadcast?, ttl?, multicastTtl?, multicastLoopback? }) => DatagramSocket | Bind a UDP socket. hostname defaults to 0.0.0.0 (all interfaces — pass 127.0.0.1 for loopback-only; a locked-down host may refuse a wildcard bind). port 0 picks an ephemeral port (read it from sock.addr). Returns a DatagramSocket — send/receive, and async-iterable of datagrams. Binding needs NetListen; sending needs Net, checked per destination. Options are set at the bind and an omitted one leaves the OS default. | const sock = bind({ hostname: "0.0.0.0", port: 5353 }) |
Client
JavaScript
import { connect } from "runtime:net"; // WinterTC connect() — returns a Socket immediately; .opened settles on connect. const sock = connect({ hostname: "example.com", port: 80 }); await sock.opened; const writer = sock.writable.getWriter(); await writer.write(new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n")); await writer.close(); // Decode through TextDecoderStream so a multi-byte character split across two // chunks is still decoded correctly. let body = ""; for await (const chunk of sock.readable.pipeThrough(new TextDecoderStream())) { body += chunk; }
Socket
| Member | Type | Description |
|---|---|---|
readable | ReadableStream<Uint8Array> | Incoming bytes. |
writable | WritableStream<Uint8Array> | Outgoing bytes; closing the writer half-closes (FIN). |
opened | Promise<SocketInfo> | Resolves once connected: { remoteAddress, remotePort, localAddress, localPort, alpn }. alpn is the negotiated TLS protocol, else null. |
closed | Promise<void> | Resolves when the socket is fully closed. |
close(reason?) | Promise<void> | Fully close the socket. reason is advisory (WinterTC) and ignored. |
startTls() | Socket | Upgrade a secureTransport: "starttls" socket to TLS in place; returns a new encrypted Socket (the original is consumed). |
upgraded | boolean | True only after a startTls() upgrade. |
TLS
JavaScript
import { connect } from "runtime:net"; // TLS client — secureTransport "on" (certificate verification on), offering ALPN. const sock = connect({ hostname: "example.com", port: 443 }, { secureTransport: "on", alpn: ["h2", "http/1.1"], }); const { alpn } = await sock.opened; // negotiated protocol, e.g. "h2" (or null)
Server
JavaScript
import { listen } from "runtime:net"; const server = listen({ hostname: "127.0.0.1", port: 8080 }); const { port } = await server.addr; for await (const conn of server) { conn.readable.pipeTo(conn.writable); // echo each connection }
JavaScript
import { listen } from "runtime:net"; // Terminate TLS on accept — cert/key are inline PEM (no extra capability). const server = listen({ hostname: "127.0.0.1", port: 8443, secureTransport: "on", cert: certPem, key: keyPem, alpn: ["h2", "http/1.1"], }); for await (const conn of server) { const { alpn } = await conn.opened; // negotiated protocol }
Listener
| Member | Type | Description |
|---|---|---|
addr | Promise<{ hostname, port }> | The bound address (resolves after bind). |
accept() | Promise<Socket | null> | The next connection, or null once closed. |
close() | Promise<void> | Stop listening. |
[Symbol.asyncIterator] | AsyncIterable<Socket> | for await (const conn of server) { … } |
UDP
JavaScript
import { bind } from "runtime:net"; const sock = bind({ hostname: "0.0.0.0", port: 5353 }); const { port } = await sock.addr; for await (const { data, address, port } of sock) { await sock.send(data, { hostname: address, port }); // echo to the sender }
JavaScript
import { bind } from "runtime:net"; // A connected socket: sends need no address, and only the peer is heard. const statsd = bind({ hostname: "0.0.0.0", port: 0 }); await statsd.connect("127.0.0.1:8125"); await statsd.send("page.view:1|c"); // Multicast: membership is dynamic, the socket options are set at the bind. const mdns = bind({ hostname: "0.0.0.0", port: 5353, reuseAddress: true, multicastTtl: 1, multicastLoopback: false, }); await mdns.joinMulticast("224.0.0.251", { interface: "192.168.1.10" });
DatagramSocket
| Member | Type | Description |
|---|---|---|
send(data, address?) | Promise<number> | Send one datagram; resolves with the bytes sent. address is "host:port" or { hostname, port }, required unless connected. Net, checked per destination. |
sendMany(messages, address?) | Promise<number> | Send a batch in one crossing; resolves with how many left. Entries are payloads or { data, address }. Saves the crossing, not the syscalls. |
receive() | Promise<Datagram | null> | The next { data, address, port, truncated }, or null once closed. One call is one message — a zero-length datagram is a message, not an EOF. |
receiveMany(max?) | Promise<Datagram[] | null> | A datagram plus up to max - 1 more already queued (default 32). Never waits to fill a batch. |
connect(address) | Promise<SocketInfo> | Fix the peer: sends need no address, datagrams from anyone else are discarded. No packet is sent, so it succeeds against a host that is not listening. |
joinMulticast(group, options?) | Promise<void> | Join a group. options.interface is an IPv4 address for a v4 group, an interface index for a v6 one; options.source makes it source-specific (RFC 4607, IPv4) so the network filters everyone else out. |
leaveMulticast(group, options?) | Promise<void> | Leave a group — with the same source, if it had one. |
setTtl(n) / setMulticastTtl(n) | Promise<void> | Hop limits, after the bind. |
setBroadcast(on) / setMulticastLoopback(on) | Promise<void> | The two toggles, after the bind. |
setMulticastInterface(iface) | Promise<void> | Which local interface carries outgoing multicast. No bind-time twin. |
ref() / unref() | this | Whether a pending receive() keeps the process alive. |
addr | Promise<{ hostname, port }> | The bound address. |
close() | Promise<void> | Close; a parked receive() resolves to null. |
closed | Promise<void> | Resolves once closed. |
[Symbol.asyncIterator] | AsyncIterable<Datagram> | for await (const d of sock) { … }, ending at close(). |
Bind options
| Option | Type | Description |
|---|---|---|
reusePort | boolean | Share the address across processes (SO_REUSEPORT). Unix only — refused elsewhere. |
reuseAddress | boolean | Share it with another socket (SO_REUSEADDR) — what two processes receiving one multicast group need. |
broadcast | boolean | Permit sending to the broadcast address. IPv4 only; on a v6 socket it is an error. |
ttl | number | Hop limit for unicast datagrams, 0–255. |
multicastTtl | number | Hop limit for multicast datagrams, 0–255. The OS default is 1: the local segment. |
multicastLoopback | boolean | Whether multicast sends come back to this host. Off means a sender does not hear its own announcements. |
ipv6Only | boolean | For a v6 bind: IPv6 only, or also IPv4 through v4-mapped addresses. Omitted leaves the platform default, which differs between platforms. |
reusePort, reuseAddress and ipv6Only are bind-time only — they must be set between socket() and bind(). The rest have setters.
Errors
| Error | When |
|---|---|
TypeError | Any socket failure — invalid options, or a connect / TLS / I/O error (the latter reject .opened or the streams). The message is prefixed "SocketError: " (WinterTC SocketError). |
DOMException | name "NotAllowedError" — the Net (connect, send) or NetListen (listen, bind) capability is not granted. |