March Docs

PeerReader

PeerReader: ONE reader per peer connection, dispatching frames by tag.

Distributed actors 2/4, step 1 (the per-peer receive loop): specs/progress/2026-09-14-distributed-plane-flow-control-and-control-channel.md.

Until this module there was no receive loop at all. Every consumer read its own frames off the fd it was handed -- NodeCall.recv_reply, NodeSend.recv_failure, the discovery fixture's SwimDriver.decode_msg -- and each SKIPPED the frames it did not recognise. Two consumers on one connection therefore stole each other's frames: a MONITOR_FIRE read by a NodeSend loop waiting for a DELIVERY_FAILED was simply gone. A frame must be read once, by one reader, and handed to the consumer its tag names.

Tag space (the first element of the frame's msgpack array, an Int): 0..3 SWIM (SwimDriver) 5,6 REGISTRY_SYNC_REQ/RESP (GlobalRegistry) 7,8 MONITOR_REQ/FIRE (DistLink) 9,10 ACTOR_MSG/DELIVERY_FAILED (NodeSend) RPC frames (RemoteCall) carry NO tag: a request begins with the module name (a Str) and a reply with its correlation number (an Int that lands anywhere in the tag space). tag_of reports a request as untagged(); a reply is indistinguishable from a tagged frame by its first element, so RPC keeps its own connection (as NodeCall already requires) until the control/data split gives it a tagged envelope. That is the one wire change the split will make.

The dispatch is INJECTED, as in NodeRpc and NodeSend: a library cannot name an actor's constructors, so the caller's on_frame(tag, frame) decides what each tag means on this connection. Returning Err stops the loop (the reason is returned); returning Ok(false) stops it cleanly.

Functions

fnserveserve(fd : Int, buf : List(Int), on_frame : Int -> List(Int) -> Result(Bool, String)) : Result((), String)#
fntag_oftag_of(frame : List(Int)) : Int#
fnuntaggeduntagged() : Int do -1 end#