feat(channels): core protocol machinery — v0.6 chunk 1
Feature 'channels' (zero new deps): wire-neutral ChannelFrame/FrameRef + Encode/Decode codec traits (one method each, whole-envelope — the codec owns the wire format end to end); Channel trait; ChannelFactory + TopicRouter + shipped PrefixRouter (exact map + head:* prefix map); ChannelSocket (reply/push/broadcast/broadcast_from/broadcast_to); ChannelHub (router + PubSub<Broadcast<P>>, in-runtime + non-static per the v0.5 pattern, hub.upgrade(conn) entry, server-side hub.broadcast). Topology as ratified: one channel actor per (socket, topic), linked to the conn actor, select(&[&inbound, &bus_rx]) with inbound at index 0; subscription pinned to the channel actor pid. Subscribe-before-ok-reply makes the join ack first-class (the v0.5 on_open race, answered structurally). Cleanup is the sender-drop chain (conn handler drops -> inbound closes -> closed arm wakes -> terminate); bus arm dropped from the select set once observed closed (closed arms stay ready forever). Heartbeat answered conn-side on topic 'phoenix'; rejoin replaces the old generation with a phx_close to the old join_ref; lazy prune of dead channel entries on first failed forward (the pubsub discipline). As-landed deviations from the ratified sketch (module docs, veto by diff): join takes &mut self (trait-object callable; construction moved to ChannelFactory which still gets the full raw topic); reply(status, payload) with the ref threaded invisibly (the sketch listed both); broadcast gained the event parameter (unencodable without it); route() returns Arc not Box. Outbound payloads encode from borrows (FrameRef) so bus fan-out never clones P; payload-less control frames (phx_close, error/heartbeat replies) encode as the codec's empty payload rather than conjuring a P. 11 runtime-backed unit tests with a toy pipe codec (core provably JSON-free). Suites: 78 w/ feature, 67 featureless, both green.
This commit is contained in: