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

FunctionTypeDescriptionExample
connect(address, options?)(Address, { secureTransport?, sni?, alpn?, allowHalfOpen? }) => SocketOpen 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? }) => ListenerBind 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? }) => DatagramSocketBind 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 DatagramSocketsend/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

MemberTypeDescription
readableReadableStream<Uint8Array>Incoming bytes.
writableWritableStream<Uint8Array>Outgoing bytes; closing the writer half-closes (FIN).
openedPromise<SocketInfo>Resolves once connected: { remoteAddress, remotePort, localAddress, localPort, alpn }. alpn is the negotiated TLS protocol, else null.
closedPromise<void>Resolves when the socket is fully closed.
close(reason?)Promise<void>Fully close the socket. reason is advisory (WinterTC) and ignored.
startTls()SocketUpgrade a secureTransport: "starttls" socket to TLS in place; returns a new encrypted Socket (the original is consumed).
upgradedbooleanTrue 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

MemberTypeDescription
addrPromise<{ 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

MemberTypeDescription
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()thisWhether a pending receive() keeps the process alive.
addrPromise<{ hostname, port }>The bound address.
close()Promise<void>Close; a parked receive() resolves to null.
closedPromise<void>Resolves once closed.
[Symbol.asyncIterator]AsyncIterable<Datagram>for await (const d of sock) { … }, ending at close().

Bind options

OptionTypeDescription
reusePortbooleanShare the address across processes (SO_REUSEPORT). Unix only — refused elsewhere.
reuseAddressbooleanShare it with another socket (SO_REUSEADDR) — what two processes receiving one multicast group need.
broadcastbooleanPermit sending to the broadcast address. IPv4 only; on a v6 socket it is an error.
ttlnumberHop limit for unicast datagrams, 0–255.
multicastTtlnumberHop limit for multicast datagrams, 0–255. The OS default is 1: the local segment.
multicastLoopbackbooleanWhether multicast sends come back to this host. Off means a sender does not hear its own announcements.
ipv6OnlybooleanFor 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

ErrorWhen
TypeErrorAny 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).
DOMExceptionname "NotAllowedError" — the Net (connect, send) or NetListen (listen, bind) capability is not granted.
Last updated on
Edit this page