char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7a27268
main
Raw networking
connect and listen from avril:std provide plaintext TCP and Unix streams.
TLS is not yet implemented.
import { listen } from "avril:std" ; const listener = listen ({ hostname :"127.0.0.1" , port :9000 }); for await ( const connection of listener ) { // Handle connections concurrently if one must not block accepting the next. connection . readable . pipeTo ( connection . writable ). finally (() => connection . close ()); }
import { connect } from "avril:std" ; const connection = await connect ({ hostname :"127.0.0.1" , port :9000 }); const writer = connection . writable . getWriter (); await writer . write ( new TextEncoder (). encode ( "hello" )); await writer . close (); // Send FIN; receiving remains possible. writer . releaseLock (); const reader = connection . readable . getReader (); while ( true ) { const { value, done} = await reader . read (); if ( done ) break ; console . log ( value ); } reader . releaseLock (); connection . close ();
Addresses and permissions
Both functions default to hostname 127.0.0.1; port is required. listen permits
port 0, while connect requires a nonzero port. Addresses are immutable
{ transport: "tcp", hostname, port } objects. A listener exposes addr; a
connection exposes localAddr and remoteAddr.
TCP requires net authority for the requested hostname/port. Port-zero listeners
check both port 0 and the selected port; use a hostname-wide grant. Accepting and
using accepted connections check listener authority, not an outbound grant for the
peer. DNS lookup and connection use the requested hostname's authority.
Every accept, read, write, and write shutdown rechecks authority. Connect, accept,
and read also recheck after awaiting I/O, before releasing results. pledge and
worker restrictions apply to existing resources. Closing never requires authority.
The CLI HTTP listener exemption does not apply to raw sockets.
Unix streams
const listener = listen ({ transport :"unix" , path :"/tmp/avril.sock" }); const connection = await connect ({ transport :"unix" , path :"/tmp/avril.sock" });
Unix streams use the same Conn, half-close, byte-stream, cancellation, and iterator
APIs as TCP. Specify transport: "unix" and a nonempty path; hostname and port are
not accepted. Relative paths resolve against the runtime's working directory.
Addresses are { transport: "unix", path }; unnamed local/peer addresses have
path: null.
Both connecting and listening require filesystem write authority at the socket
path, not net or filesystem read authority. Connecting resolves the final symlink
and checks authority at its target. Accepts and subsequent connection I/O recheck
that same write authority. Closing and unlinking do not need a fresh write grant.
Listeners never overwrite existing filesystem entries. Cleanup checks device/inode identity and preserves replacements. Linux listeners pin their parent directory and inode for binding/cleanup. Other Unix systems use the documented path-based, race-prone fallback shared with HTTP serving. Unix connects perform path-based lookups on every platform and can also race with filesystem changes after the authority check. These APIs are not supported on non-Unix platforms.
UDP datagrams
import { listenDatagram } from "avril:std" ; const socket = listenDatagram ({ hostname :"127.0.0.1" , port :9001 }); for await ( const [ packet , peer ] of socket ) { await socket . send ( packet , peer ); }
listenDatagram({ transport?: "udp", hostname?, port }) returns a
DatagramConn with immutable addr: { transport: "udp", hostname, port }.
The default hostname is 127.0.0.1; port 0 selects an available port.
send(bytes, { hostname?, port, transport?: "udp" }) also defaults to loopback.
Binding and receiving require net authority for the bound hostname/port.
Every send also requires authority for its destination hostname/port, checked
before DNS and again before sending. Hostname/port grants cover both TCP and UDP.
DNS chooses an address matching the socket's IPv4/IPv6 family. The CLI HTTP
exemption does not apply.
receive(buffer?) returns [Uint8Array, UdpAddr]. With a supplied buffer, the
returned view shares that buffer; an oversized incoming packet is truncated and
the remainder discarded, not carried into the next receive. An empty buffer still
consumes a packet. Empty datagrams return an empty array with a peer address:
they are not EOF. Without a buffer, receives allow up to 65,535 bytes.
send snapshots its payload, preserves message boundaries, and returns the number
of bytes sent. Sends are ordered in invocation order; UDP itself does not guarantee
delivery or ordering. Payloads over the portable 65,507-byte limit are rejected
rather than split. IP fragmentation may still happen below that limit.
setBroadcast(enabled = true) controls IPv4 broadcast permission on the socket;
each broadcast destination still needs its own network grant. Multicast membership
and Unix datagrams are not implemented.
Closing cancels pending receives and sends; disposal is idempotent and handles cannot be structured-cloned. The async iterator ends on close and closes the socket when its loop is broken. Worker termination releases the socket. Merely binding does not keep the event loop alive; a pending receive does.
Stream I/O and lifetime
Conn.read(Uint8Array) returns a byte count or null for EOF. An empty buffer
returns 0. Conn.write(Uint8Array) snapshots its input and may write fewer bytes
than requested, with a maximum of 64 KiB per call. Loop over partial writes, or use
writable, which writes each chunk completely with backpressure. Writes and
closeWrite() are ordered in invocation order.
Conn.writev(readonly Uint8Array[]) writes one ordered vector as a single queued
operation: the byte sequence equals the concatenation of its views, empty views
are skipped, and unlike write the call completes the entire vector before
resolving with the total bytes written. Later writes and closeWrite() cannot
interleave between its batches, and an error may leave a prefix written without
rolling it back.
The whole vector is validated and snapshotted at invocation, so mutating the array or its views afterwards cannot affect any submitted bytes. An empty or all-empty vector returns 0, still performing the usual closed and authority checks. Snapshots retain the entire payload until completion; await writes rather than queueing unbounded batches.
readable is a byte stream supporting BYOB readers. Its EOF does not close the
write half. closeWrite() and closing writable send FIN without closing the read
half; repeated write shutdown is harmless. Reading and writing can proceed
simultaneously. Do not mix direct reads with a stream reader.
close() closes both halves and cancels pending I/O. Canceling readable or
aborting writable also closes the connection. Stream errors close the connection.
Sockets and listeners support idempotent [Symbol.dispose]() and cannot be
structured-cloned. Close resources explicitly; garbage collection is only a
fallback. Worker termination closes the worker's sockets and pending operations.
Listener.accept() returns a connection. Its async iterator ends when the listener
closes; breaking the loop closes the listener, but not connections already accepted.
A pending accept keeps the event loop alive; merely binding a listener does not.
connect({ ..., signal }) supports aborting an in-progress connect, including DNS
lookup (the OS resolver itself may continue after cancellation). An already-aborted
signal prevents networking. The signal does not own the
connection once connect returns; use connection.close() to cancel later I/O.
1# Raw networking 2 3`connect` and `listen` from `avril:std` provide plaintext TCP and Unix streams. 4TLS is not yet implemented. 5 6``` ts 7import { listen } from "avril:std"; 8 9const listener = listen({ hostname: "127.0.0.1", port: 9000 }); 10for await (const connection of listener) { 11// Handle connections concurrently if one must not block accepting the next. 12connection.readable.pipeTo(connection.writable).finally(() => connection.close()); 13} 14``` 15 16``` ts 17import { connect } from "avril:std"; 18 19const connection = await connect({ hostname: "127.0.0.1", port: 9000 }); 20const writer = connection.writable.getWriter(); 21await writer.write(new TextEncoder().encode("hello")); 22await writer.close(); // Send FIN; receiving remains possible. 23writer.releaseLock(); 24const reader = connection.readable.getReader(); 25while (true) { 26const { value, done } = await reader.read(); 27if (done) break; 28console.log(value); 29} 30reader.releaseLock(); 31connection.close(); 32``` 33 34## Addresses and permissions 35 36Both functions default to hostname `127.0.0.1`; port is required. `listen` permits 37port `0`, while `connect` requires a nonzero port. Addresses are immutable 38`{ transport: "tcp", hostname, port }` objects. A listener exposes `addr`; a 39connection exposes `localAddr` and `remoteAddr`. 40 41TCP requires `net` authority for the requested hostname/port. Port-zero listeners 42check both port `0` and the selected port; use a hostname-wide grant. Accepting and 43using accepted connections check listener authority, not an outbound grant for the 44peer. DNS lookup and connection use the requested hostname's authority. 45 46Every accept, read, write, and write shutdown rechecks authority. Connect, accept, 47and read also recheck after awaiting I/O, before releasing results. `pledge` and 48worker restrictions apply to existing resources. Closing never requires authority. 49The CLI HTTP listener exemption does not apply to raw sockets. 50 51## Unix streams 52 53``` ts 54const listener = listen({ transport: "unix", path: "/tmp/avril.sock" }); 55const connection = await connect({ transport: "unix", path: "/tmp/avril.sock" }); 56``` 57 58Unix streams use the same `Conn`, half-close, byte-stream, cancellation, and iterator 59APIs as TCP. Specify `transport: "unix"` and a nonempty path; hostname and port are 60not accepted. Relative paths resolve against the runtime's working directory. 61Addresses are `{ transport: "unix", path }`; unnamed local/peer addresses have 62`path: null`. 63 64Both connecting and listening require filesystem **write** authority at the socket 65path, not `net` or filesystem read authority. Connecting resolves the final symlink 66and checks authority at its target. Accepts and subsequent connection I/O recheck 67that same write authority. Closing and unlinking do not need a fresh write grant. 68 69Listeners never overwrite existing filesystem entries. Cleanup checks device/inode 70identity and preserves replacements. Linux listeners pin their parent directory 71and inode for binding/cleanup. Other Unix systems use the documented path-based, 72race-prone fallback shared with HTTP serving. Unix connects perform path-based 73lookups on every platform and can also race with filesystem changes after the 74authority check. These APIs are not supported on non-Unix platforms. 75 76## UDP datagrams 77 78``` ts 79import { listenDatagram } from "avril:std"; 80 81const socket = listenDatagram({ hostname: "127.0.0.1", port: 9001 }); 82for await (const [packet, peer] of socket) { 83await socket.send(packet, peer); 84} 85``` 86 87`listenDatagram({ transport?: "udp", hostname?, port })` returns a 88`DatagramConn` with immutable `addr: { transport: "udp", hostname, port }`. 89The default hostname is `127.0.0.1`; port `0` selects an available port. 90`send(bytes, { hostname?, port, transport?: "udp" })` also defaults to loopback. 91 92Binding and receiving require `net` authority for the bound hostname/port. 93Every send also requires authority for its **destination** hostname/port, checked 94before DNS and again before sending. Hostname/port grants cover both TCP and UDP. 95DNS chooses an address matching the socket's IPv4/IPv6 family. The CLI HTTP 96exemption does not apply. 97 98`receive(buffer?)` returns `[Uint8Array, UdpAddr]`. With a supplied buffer, the 99returned view shares that buffer; an oversized incoming packet is truncated and 100the remainder discarded, not carried into the next receive. An empty buffer still 101consumes a packet. Empty datagrams return an empty array with a peer address: 102they are not EOF. Without a buffer, receives allow up to 65,535 bytes. 103 104`send` snapshots its payload, preserves message boundaries, and returns the number 105of bytes sent. Sends are ordered in invocation order; UDP itself does not guarantee 106delivery or ordering. Payloads over the portable 65,507-byte limit are rejected 107rather than split. IP fragmentation may still happen below that limit. 108 109`setBroadcast(enabled = true)` controls IPv4 broadcast permission on the socket; 110each broadcast destination still needs its own network grant. Multicast membership 111and Unix datagrams are not implemented. 112 113Closing cancels pending receives and sends; disposal is idempotent and handles 114cannot be structured-cloned. The async iterator ends on close and closes the socket 115when its loop is broken. Worker termination releases the socket. Merely binding 116does not keep the event loop alive; a pending receive does. 117 118## Stream I/O and lifetime 119 120`Conn.read(Uint8Array)` returns a byte count or `null` for EOF. An empty buffer 121returns `0`. `Conn.write(Uint8Array)` snapshots its input and may write fewer bytes 122than requested, with a maximum of 64 KiB per call. Loop over partial writes, or use 123`writable`, which writes each chunk completely with backpressure. Writes and 124`closeWrite()` are ordered in invocation order. 125 126`Conn.writev(readonly Uint8Array[])` writes one ordered vector as a single queued 127operation: the byte sequence equals the concatenation of its views, empty views 128are skipped, and unlike `write` the call completes the entire vector before 129resolving with the total bytes written. Later writes and `closeWrite()` cannot 130interleave between its batches, and an error may leave a prefix written without 131rolling it back. 132 133The whole vector is validated and snapshotted at invocation, so mutating the 134array or its views afterwards cannot affect any submitted bytes. An empty or 135all-empty vector returns 0, still performing the usual closed and authority 136checks. Snapshots retain the entire payload until completion; await writes 137rather than queueing unbounded batches. 138 139`readable` is a byte stream supporting BYOB readers. Its EOF does not close the 140write half. `closeWrite()` and closing `writable` send FIN without closing the read 141half; repeated write shutdown is harmless. Reading and writing can proceed 142simultaneously. Do not mix direct reads with a stream reader. 143 144`close()` closes both halves and cancels pending I/O. Canceling `readable` or 145aborting `writable` also closes the connection. Stream errors close the connection. 146Sockets and listeners support idempotent `[Symbol.dispose]()` and cannot be 147structured-cloned. Close resources explicitly; garbage collection is only a 148fallback. Worker termination closes the worker's sockets and pending operations. 149 150`Listener.accept()` returns a connection. Its async iterator ends when the listener 151closes; breaking the loop closes the listener, but not connections already accepted. 152A pending accept keeps the event loop alive; merely binding a listener does not. 153 154`connect({ ..., signal })` supports aborting an in-progress connect, including DNS 155lookup (the OS resolver itself may continue after cancellation). An already-aborted 156signal prevents networking. The signal does not own the 157connection once `connect` returns; use `connection.close()` to cancel later I/O.