velt/net
import { listen, connect } from "velt/net". TCP, and TLS over TCP. TcpListener and
TcpStream are structs around a handle, like file descriptors. Release each handle with
close().
Its submodules: velt/net/http (fetch and the web types),
velt/net/websocket, velt/net/dns,
velt/net/udp and velt/net/url.
-
listen(addr: "host:port"): Promise<TcpListener>: port 0 picks a free port. The listen backlog is the OS maximum (a burst of connects waits to be accepted).connect(addr): Promise<TcpStream>. -
TcpListener { port: number }:accept(): Promise<TcpStream>(TCP_NODELAY on),close()(anacceptstill waiting then fails withEBADF). -
TcpStream:read(max = 0): Promise<u8[]>: empty means end of stream.readString(max = 0): Promise<string>: UTF-8; a character split across reads is completed by the next read.readInto(buf: Uint8Array): Promise<number | null>: reads what has arrived intobuf(at mostbuf.lengthbytes) and returns how many, ornullat the end of the stream, as Deno'sconn.read. It waits only when nothing has arrived yet, and a plain TCP stream reads straight intobuf: nothing is allocated or copied on the way. Reuse one buffer per connection and parse withsubarrayviews of what was read. An emptybufreturns 0.readandreadIntomay be mixed on one stream.tryRead(buf: u8[] | Uint8Array, start = 0, end = buf.length): number | null: reads what has arrived intobuf[start..end]without waiting: the bytes read,0when nothing has arrived yet,nullat the end of the stream (RangeErrorfor an empty or out-of-range range). Withreadable(): Promise<void>, which waits until atryReadmay find something, a parser reads straight into its ownu8[]buffer.write(data: string),writeBytes(data: u8[] | Uint8Array, start = 0, end = data.length): bytes go out in the order of the calls, also when a write is started and not awaited yet. What the socket takes at the call is written fromdataitself; only the rest is copied. AUint8Arraywrites its own range (asubarraywrites just that part);startandendwrite part of either (taken assubarraytakes them: a negative one counts from the end, and both are clamped to it), so a reply buffer kept per connection goes out without a copy.tryWrite(data: u8[] | Uint8Array, start = 0, end = data.length): i64: writes what the socket takes right now, without waiting, and returns the number of bytes written (0 when it takes nothing now, while an earlier write is still going on, for a TLS stream, and after an error, which the nextwritereports). When everything was written there is nothing to await.writeLater(data: string): i64,writeBytesLater(data: u8[] | Uint8Array, start = 0, end = data.length): i64: queue bytes (copied at the call) that are sent when the task next waits or finishes, together with everything else it queued until then: one system call for a response head and the body pieces a render produced without waiting, and nothing held back past a wait. They return the number of bytes still to be sent on the stream. Awritegoes after what was queued before it. They throwIoErroronce sending failed (the error awritewould report), afterabort()and on a closed handle.drained(max = 0): Promise<void>: resolves once at mostmaxbytes queued bywriteLater(or written after them) are still to be sent, at once when there are no more: keeps a producer that queues faster than the peer reads bounded. ThrowsIoErrorwhen sending failed.shutdown(): half-close, after every byte written or queued before it (a TLS stream then sends its close_notify).abort(): fails the operations in flight (awriteto a peer that stopped reading would otherwise never finish) and every later one. The handle stays open untilclose().readableWithin(ms, signal?: AbortSignal): Promise<bool>:readable()with a limit: true once atryReadmay find something, false whenmsmilliseconds passed first orsignalwas aborted. One wait in the runtime, so nothing is left behind (aPromise.raceofreadable()and a timer leaves the losing wait running): a server waiting for each idle connection's next request.peerAddr(): string,close(): releases the handle; a read orreadable()still waiting then fails withEBADF(no code can read what it would bring), a write in flight goes on, and so do bytes queued withwriteLater(await drained()first to know they were sent).
-
connectTls(addr, opts?: TlsConnectOptions { serverName?; ca?; alpn?: string[] }): Promise<TcpStream>: a TLS connection (rustls), resolved once the handshake is done. The certificate is checked for the host inaddr(orserverName) against the built-in roots plus the PEM certificates inca. -
listenTls(addr, opts: TlsListenOptions { cert; key; alpn?: string[] }): Promise<TcpListener>: a listener whoseacceptgives TLS streams (certandkeyare PEM text; bad PEM throwsIoErrorEINVAL).acceptreturns as soon as the TCP connection is there: the handshake runs with the stream's first read or write, which throws if it fails or takes more than 10 s. -
streamPair(): [TcpStream, TcpStream]: two connected in-memory streams, as Node'sduplexPair(): what one writes the other reads, one'sshutdown()is the other's end of stream; close both. For a protocol carried inside another one (the HTTP/2 server runs a WebSocket over an HTTP/2 stream this way).
A TLS stream is a TcpStream like any other: one task can read while another writes, and
shutdown() sends TLS's close_notify. alpn lists the protocols to offer, most preferred
first, and await stream.alpnProtocol() is the one the handshake chose ("h2",
"http/1.1"), or null when none was agreed or the stream is not TLS (Node's
socket.alpnProtocol): it waits for the handshake, running it if no read or write did yet, and
throws if it fails. A server that offers HTTP/2 and HTTP/1.1 reads it to know which one the
client speaks:
import { listenTls, TcpListener } from "velt/net";
async function protocolOf(l: TcpListener): Promise<string> {
const conn = await l.accept();
const p = await conn.alpnProtocol();
conn.close();
return p ?? "http/1.1";
}import { listen, connect, TcpListener } from "velt/net";
async function echoOnce(l: TcpListener): Promise<void> {
const conn = await l.accept();
const msg = await conn.readString();
await conn.write(msg.toUpperCase());
conn.close();
}
const l = await listen("127.0.0.1:0");
const server = spawn(echoOnce(l));
const c = await connect(`127.0.0.1:${l.port}`);
await c.write("ping");
console.log(await c.readString()); // PING
c.close();
await server;
l.close();import { listenTls, connectTls } from "velt/net";
import { readFile } from "velt/fs";
const l = await listenTls("127.0.0.1:0", {
cert: await readFile("cert.pem"),
key: await readFile("key.pem"),
});
const server = spawn(async () => {
const conn = await l.accept();
await conn.write(`hello, ${await conn.readString()}`);
conn.close();
});
const c = await connectTls(`localhost:${l.port}`, { ca: await readFile("ca.pem") });
await c.write("world");
console.log(await c.readString()); // hello, world
c.close();
await server;
l.close();A connection an HTTP server hands over for another protocol (upgradeConnection in the
HTTP server package) is a TcpStream too, and so is every
connection a server built on listen or listenTls accepts: velt/net/http
has what such a server needs to speak HTTP with the web types.
Notes: an unclosed handle leaks until the process exits. After close() through any copy
(also one handed to another task), every copy's operations throw IoError EBADF and closing
again does nothing; operations already in flight finish first. Planned: these structs become disposable classes with using support.