Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3.4 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 fordead_aftermeans 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.
gatewaydthen reconnects (task 14); nothing panics.
Files
- Copy:
crates/gatewayd/tests/ws_conn.rs,crates/gatewayd/tests/support/ws_server.rs, and the skeletoncrates/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>;
pub fn close(mut self);
}
pub fn host_header(server: &ServerUrl) -> String;
All are todo!(), each with its steps above it. poll is the one with the most in 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. Write it as its comment says, step by step; a helper
function for step 1 is fine.
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, thencp 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/. Addpub mod conn;tocrates/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, thenpoll,cargo check -p gatewaydafter 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, thenmake 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_connpasses five times running;make gateprintsgate: ok.
Stop and report if
- A test passes only sometimes, or takes seconds.