UDP sockets, for multiplayer, livecoding, and streaming data.
A socket is bound once and kept in the model. bind! and send! are legal
in init!, update!, and tasks, and refused in render!, which draws.
receive! waits: it is legal in init!, where it blocks startup, and in
tasks, where it parks the task; it is refused in update! and render!.
So sending belongs in update! beside the rest of a frame's work, and
receiving belongs in a task:
listening is set from whether this cycle's input answered rather than
latched to true, so the next update! starts the next listener. A failed
send! is counted rather than propagated: WouldBlock is a full send
buffer, an ordinary condition on a busy link, and a position update would
rather skip a frame than end the app.
A task delivers exactly one message, so the loop is "receive, answer, and
let update! start the next one" rather than a task that loops forever. A
task that never returns never delivers anything.
That is also why receive! answers with a whole batch. It parks until the
first datagram arrives and then drains what the kernel already has, so one
message carries a frame's worth of traffic in arrival order. A receive that
answered with a single datagram would cap a respawned listener at one
datagram per frame, which is far below what even a small game needs.
Datagrams that arrive while no receive! is pending are not lost: they wait
in the operating system's own buffer, and the next receive! returns them
immediately. They are lost when that buffer overflows, silently, with no
report -- that is the UDP contract, and sequence numbers in the payload are
the app's answer to it. Delivery, ordering, and duplication are not promised
by the protocol and are not added here.
Framing, retransmission, serialization, discovery, and session state are the
app's or a package's. This module moves bytes and nothing else.
Addresses are IPv4 literals. An IPv6 address is InvalidAddress, and there
is no name resolution: bind! and send! take a dotted-quad string, never
a hostname, because resolving a name waits and neither of these effects
does.
This grants the app the network authority the process already has: any port
it may bind, any host it may send to. Ports below 1024 usually need
privileges, and report PermissionDenied when they are missing. Broadcast
and multicast are not enabled.
The deadline is short enough that a listener respawned every frame keeps
its socket's queue drained even when a peer goes quiet, and long enough
not to churn tasks at idle.
The largest payload one datagram may carry: 65535 bytes of IPv4 packet
less the 20-byte IP header and the 8-byte UDP header. A send over this
is TooLarge.
Sending anywhere near it is unwise on a real network, where anything
over the path MTU is fragmented and one lost fragment loses the whole
datagram. Real-time protocols stay under about 1200 bytes.
{ ip: "0.0.0.0", port: 0 } takes any interface and lets the operating
system choose the port; the returned socket's local_address reports
what it chose. Bind once, in init! or when the app decides to start
networking, and keep the socket in the model -- binding per frame would
exhaust the eight-socket ceiling and change the port every time.
Legal in init!, update!, and tasks; refused in render!.
timeout_ms is how long to wait for the first datagram; 0 waits
forever, which only makes sense in a task the app is content to leave
parked until shutdown. max_datagrams caps the batch, and the host
clamps it to sixty-four -- past that the rest simply stay buffered for
the next receive.
A plain record; build one with { ..Udp.default_receive, timeout_ms: 200 }
rather than a chain of with_* calls.
AddressInUse is another socket already holding the port,
AddressUnavailable is an address that is not one of this machine's,
and ResourceLimit is this platform's own ceiling of eight open
sockets. InvalidAddress is a string that is not a dotted-quad IPv4
literal.
WouldBlock is the send buffer being full: the datagram was not sent,
and the app is producing faster than the link can carry. TooLarge is a
payload over max_datagram_bytes, refused rather than truncated,
because a truncated datagram decodes into wrong data. None of these mean
the peer received anything, and no code means it did -- UDP does not
report that.
Timeout is the deadline expiring with nothing having arrived, which is
an ordinary quiet moment rather than a failure. AlreadyReceiving is a
second task trying to receive on a socket that already has one parked.
The handle is reference counted: copy it freely, and when the last copy
goes -- out of the model, out of a task's captures, or at shutdown --
the socket is closed. There is nothing to remember to close.
Send one datagram. This does not wait for anything: the payload goes
to the operating system and the call is over, whether or not any
peer is listening. Nothing here reports that a datagram arrived,
because UDP does not.
It is a queued effect: the datagram is handed whole to the kernel's
bounded send buffer and the call returns at once, with the sending
itself happening off the frame thread. WouldBlock is that queue's
saturation -- the buffer was full, so this datagram was not taken --
and it is a typed result rather than a wait or a silent drop.
Legal in init!, update!, and tasks; refused in render!.
Wait for datagrams, and answer with every one that was ready.
Parks until the first datagram arrives or timeout_ms expires, then
returns it together with whatever else is already buffered, in
arrival order, up to max_datagrams. Err(Timeout) means nothing
arrived in time, which is a quiet moment rather than a failure.
One receive at a time per socket: a second one while another task is
parked is AlreadyReceiving.
Legal in init!, where it blocks startup, and in tasks, where it
parks the task; refused in update! and render!.
The handle never resolves to an open socket, so every effect made
through it fails as Unavailable -- Err(Unavailable) from send!
and from receive! -- the same way a call through a closed socket
does. It exists for the app that keeps a socket in its model, to let
a pure expect build that model. Do not use it to test delivery or
resource lifetime.