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>
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 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>; // 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, 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, thentake_messages,keep_alive,read_timeout,read_some,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.