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>
This commit is contained in:
2026-09-23 20:37:48 -07:00
co-authored by Claude Opus 5.5
parent d56fcf8611
commit 02681069b6
6 changed files with 227 additions and 54 deletions
+17 -12
View File
@@ -34,19 +34,24 @@ Written: `MAX_MESSAGE`, the opcode constants, `Incoming` (`Text`, `Ping`, `Pong`
`Close(Option<u16>, String)`), `Decoder { buf, partial }` with `new` and `feed`, and the private
`Header { fin, opcode, header_len, payload_len }`.
To fill, in this order, each with its steps above it:
Also written, because they are the glue: **`Decoder::next_message`** (take a whole frame out of
`buf` and act on it) and **`Decoder::header`** (read the first two bytes, then call the checks and
`length` below). Read both first: they call everything you write.
1. `Decoder::header(&self) -> Result<Option<Header>, WsError>`: the next frame's header once all of
it is in `buf`, checked against every rule above that does not need the payload. `Ok(None)`
means "wait for more bytes". It only reads `buf`; it removes nothing.
2. `close(payload) -> Result<Incoming, WsError>`.
3. `Decoder::next_message(&mut self)`: uses `header`, and when the whole frame is in `buf`, takes it
out (`drain`) and acts on it. Control frames may come between the frames of a text message and
are returned at once.
4. `encode(opcode, payload, mask) -> Vec<u8>`.
To fill, **one at a time, in this order**, each with its steps above it. Each is a few lines:
No indexing that can go out of bounds, no `as` casts: read lengths with `get(2..4)` and
`u16::from_be_bytes`, and convert with `usize::try_from` / `u8::try_from`.
1. `check_first_bytes(b0, b1)`: the reserved bits, the mask bit, the opcode.
2. `check_control(fin, payload_len)`.
3. `Decoder::length(&self, short)`: the three length forms. `Ok(None)` means "wait for more bytes".
4. `Decoder::check_data(&self, opcode, payload_len)`: the rules that depend on `partial`.
5. `Decoder::data_frame(&mut self, opcode, fin, payload)`: a text or continuation payload.
6. `close(payload)`.
7. `encode(opcode, payload, mask)`.
The comment above each `todo!()` gives the constructs to use, down to the expressions for the
indexing and the conversions. Use them as written: they are checked against the tests, and the
indexing in them cannot go out of bounds. Do not weigh other ways to write the same thing. Write
the function, run `cargo check -p gatewayd`, go on to the next.
## Steps
@@ -55,7 +60,7 @@ No indexing that can go out of bounds, no `as` casts: read lengths with `get(2..
Add `pub mod frame;` to `crates/gatewayd/src/ws/mod.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --test ws_frame`. Expected: it compiles and 7
tests fail.
- [ ] **3. Fill the four functions**, `cargo check -p gatewayd` after each.
- [ ] **3. Fill the seven functions** in the order above, `cargo check -p gatewayd` after each.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test ws_frame`. Expected: 7 passed. The last
test feeds 300 random streams, half of them with bits flipped, in random pieces, and compares
your decoder with a simple one written inside the test: they must return the same messages, and