Files
boxmaker/docs/plans/M4a/09-gatewayd-ws-conn.md
T
kyleandClaude Opus 5.5 02681069b6 M4a tasks 08 and 09: finer skeletons after task 08's session wrote nothing
Task 08's `header` was one todo!() with a dozen branches; Ornith planned it in its head until the
turn ran out (tip T25). `next_message` and `header` are now written as glue over seven small
helpers, and task 09's `poll` over four, each checked to pass when filled from its comments.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 20:37:48 -07:00

3.9 KiB

M4a task 09: the WebSocket connection

Branch: m4a (run git switch m4a; git status --short must be empty, otherwise stop) Commit subject: gatewayd: ws conn, messages, pings, closing and a dead peer

Goal

The connection that uses tasks 07 and 08: open it, send text, and wait for the next message while keeping the connection alive (spec section 6):

  • A ping from the server is answered with a pong carrying the same payload.
  • We send a ping every ping_every. No bytes at all for dead_after means the peer is dead.
  • A close frame is answered with a close frame, and the connection ends (Closed); so does the end of the stream.
  • Any protocol error ends the connection. gatewayd then reconnects (task 14); nothing panics.

Files

  • Copy: crates/gatewayd/tests/ws_conn.rs, crates/gatewayd/tests/support/ws_server.rs, and the skeleton crates/gatewayd/src/ws/conn.rs
  • Modify: crates/gatewayd/src/ws/mod.rs (pub mod conn;), docs/implementer-log.md

The skeleton

pub const PATH: &str = "/api/v4/websocket";
pub struct Timing { pub ping_every: Duration, pub dead_after: Duration }   // Clone, Copy
pub struct Ws { stream, decoder, random, timing, last_heard, last_ping }

impl Ws {
    pub fn open(connector: &Connector, token: &str, timing: Timing,
                mut random: Box<dyn Read + Send>) -> Result<Ws, WsError>;
    fn send(&mut self, opcode: u8, payload: &[u8]) -> Result<(), WsError>;
    pub fn send_text(&mut self, text: &str) -> Result<(), WsError>;
    pub fn poll(&mut self, wait: Duration) -> Result<Option<String>, WsError>;   // written
    fn take_messages(&mut self) -> Result<Option<String>, WsError>;
    fn keep_alive(&mut self, now: Instant) -> Result<(), WsError>;
    fn read_timeout(&self, until: Instant, now: Instant) -> Duration;
    fn read_some(&mut self, timeout: Duration) -> Result<(), WsError>;
    pub fn close(mut self);
}
pub fn host_header(server: &ServerUrl) -> String;

poll is written for you: it is the glue, and it calls the four helpers after it. It returns the next text message, or None after about wait with none, and does the pinging, the pong answers and the dead-peer check on the way. Its read timeout is always the time to the next thing it must do (the end of wait, the next ping, or the dead-after limit), so a quiet connection neither spins nor oversleeps. Read it first.

Everything else is todo!(), each a few lines, with the steps and the expressions to use above it. Use them as written; do not weigh other ways to write the same thing. Write the function, run cargo check -p gatewayd, go on to the next.

random gives the handshake key and a fresh 4-byte mask for every frame we send (in gatewayd, /dev/urandom).

Steps

  • 1. Copy. git switch m4a, then cp docs/plans/M4a/files/crates/gatewayd/tests/ws_conn.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/ws_server.rs crates/gatewayd/tests/support/ && cp docs/plans/M4a/files/crates/gatewayd/src/ws/conn.rs crates/gatewayd/src/ws/. Add pub mod conn; to crates/gatewayd/src/ws/mod.rs.
  • 2. See it fail. cargo test -p gatewayd --test ws_conn. Expected: it compiles and 10 tests fail.
  • 3. Fill host_header, send, send_text, open, close, then take_messages, keep_alive, read_timeout, read_some, cargo check -p gatewayd after each.
  • 4. See it pass. cargo test -p gatewayd --test ws_conn, five times. Expected: 10 passed each time, in under a second.
  • 5. Run the gate. cargo fmt --all, then make gate. Expected last line: gate: ok.
  • 6. Log and commit. git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit

Done when

  • cargo test -p gatewayd --test ws_conn passes five times running; make gate prints gate: ok.

Stop and report if

  • A test passes only sometimes, or takes seconds.