Files
boxmaker/docs/plans/M4a/14-gatewayd-serve.md
T
kyleandClaude Opus 5.5 564875f0a0 M4a tasks 14 and 15: glue written, calls tabled, after task 14's session wrote nothing
Task 14's session read the crate to learn the APIs and planned `connect` in prose until it was
cut off. `connect`, `Gateway::new` and `catch_up` are now written; the task lists every call with
its signature and makes the copy and the failing test the first actions. Task 15's comment is
rewritten one step per item. Both checked to fail, then pass when filled from their comments.

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

114 lines
7.1 KiB
Markdown

# M4a task 14: the serve loop
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: serve, the event loop, typing, catch-up and reconnecting`
## Goal
The loop that ties tasks 03 to 13 together (spec sections 8 and 9):
- **Connecting**, at start and after every loss: `GET /users/me`, open the WebSocket, wait for
`hello`, then log `gatewayd: connected to <url> as <username>`. A connection that fails is tried
again after 1, 2, 5, 10, then every 30 seconds, with one log line per attempt:
`gatewayd: cannot reach <url>: <error>; trying again in <n> s`, then a newline and
`see docs/runbook.md#mattermost-unreachable`. A refused token (401 or 403) is never tried again:
`run` returns `Stop::Auth`.
- **After a restart** (the first connection only): every turn left in flight gets
`interrupted: gatewayd restarted before the answer arrived; ask again` in its thread. A later
reconnect must not do this: those turns are still running.
- **Catching up**: the direct channel with each allowed user, and each allowed channel, from its
mark. A channel without a mark is marked "now" and not caught up: history is not answered.
- **Each post**, live or caught up: posts in channels we do not track are not recorded at all. A
tracked post is recorded as handled **before** it is acted on, so after a crash it is not
answered twice (the in-flight record reports it instead). Then it is routed (task 11): a stranger
is logged by user id and post id only, **never with the text**.
- **Turns** run on their own threads (task 13), recorded in flight while they run. When one ends,
its session's waiting messages start the next.
- **Typing**: every `typing_every_ms`, `user_typing` for every thread with a turn running.
## Files
- Copy: `crates/gatewayd/tests/serve.rs`, `crates/gatewayd/tests/serve_restart.rs`,
`crates/gatewayd/tests/support/fake_mm.rs`, `crates/gatewayd/tests/support/gateway.rs`, and the
skeletons `crates/gatewayd/src/serve/mod.rs` and `crates/gatewayd/src/serve/handle.rs`
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod serve;`), `docs/implementer-log.md`
## Before anything else
Your **first two actions** are steps 1 and 2 below: copy the files, and see the tests fail. Do not
read the other modules of `gatewayd` or `proto` first. Everything the functions you write call is
in the table below, with its signature, and the comment above each `todo!()` gives the code to
write, down to the expressions. Write each function as its comment says, run
`cargo check -p gatewayd`, and go on to the next. Do not weigh other ways to write it.
## The skeletons
`serve/mod.rs`, written: the pointers, `INTERRUPTED`, `Log`, `Stop` (`Auth`, `State`, `Start`,
`Asked`) and its `Display`, `Tuning` and its default, the `Gateway` struct, and the glue: **`run`**
(the connect-or-back-off loop), **`connect`** (`users/me`, the WebSocket, the wait for `hello`),
**`Gateway::new`** and **`Gateway::event_loop`** (finish turns, send typing, wait for an event,
handle it). To fill: `From<StateError> for Stop`, `backoff`, `sleep_unless`, `Gateway::connected`.
`serve/handle.rs`, written: the glue **`catch_up`** (which channels). To fill: `now_ms`, `post`,
`tracked`, `finished`, `typing`, `start`, `handle_post`, `catch_up_channel`.
Twelve functions, none more than about twenty lines. `run` takes the token already loaded and a
`stop` flag, so the tests need no secrets and can end it; task 15's `main` passes a flag that is
never set.
## What the functions call
| Call | Signature (from the earlier tasks) |
|---|---|
| `self.client.create_post` | `(&self, channel: &str, root: &str, message: &str) -> Result<Post, MmError>` |
| `self.client.posts_since` | `(&self, channel: &str, since: i64) -> Result<Since, MmError>`; `Since { posts: Vec<Post>, full: bool }` |
| `self.state.seen` / `knows_thread` | `(&self, id: &str) -> bool` |
| `self.state.handled` | `(&mut self, post_id: &str, channel: &str, create_at: i64) -> Result<(), StateError>` |
| `self.state.since` | `(&self, channel: &str) -> Option<i64>` |
| `self.state.mark` | `(&mut self, channel: &str, at: i64) -> Result<(), StateError>` |
| `self.state.join_thread` | `(&mut self, root: &str) -> Result<(), StateError>` |
| `self.state.start_turn` | `(&mut self, turn: InFlight) -> Result<(), StateError>`; `InFlight { session, channel, root }`, all `String` |
| `self.state.end_turn` | `(&mut self, session: &str) -> Result<(), StateError>` |
| `self.state.take_in_flight` | `(&mut self) -> Result<Vec<InFlight>, StateError>` |
| `self.router.route` | `(&self, post: &Post, channel_type: &str, known: &dyn Fn(&str) -> bool) -> Route` |
| `Router::new` | `(me_id: &str, me_name: &str, users: &[String], channels: &[String]) -> Router` |
| `self.queues.push` | `(&mut self, message: Message) -> Pushed` |
| `self.queues.finish` | `(&mut self, session: &SessionId) -> Option<Batch>` |
| `self.queues.threads` | `(&self) -> Vec<Thread>`; `Thread { channel, root }` |
| `deliver` | `(poster: &dyn Poster, socket: &Path, batch: &Batch, log: &dyn Fn(&str))`; `Client` is a `Poster` |
| `typing` | `(seq: u64, channel: &str, parent: &str) -> String` |
| `ws.send_text` | `(&mut self, text: &str) -> Result<(), WsError>` |
| `self.log` | `Arc<dyn Fn(&str) + Send + Sync>`: call it as `(self.log)(&line)` |
`?` turns a `StateError` into a `Stop` through the `From` you write first.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`mkdir -p crates/gatewayd/src/serve && cp docs/plans/M4a/files/crates/gatewayd/src/serve/mod.rs docs/plans/M4a/files/crates/gatewayd/src/serve/handle.rs crates/gatewayd/src/serve/`,
`cp docs/plans/M4a/files/crates/gatewayd/tests/serve.rs docs/plans/M4a/files/crates/gatewayd/tests/serve_restart.rs crates/gatewayd/tests/`
and
`cp docs/plans/M4a/files/crates/gatewayd/tests/support/fake_mm.rs docs/plans/M4a/files/crates/gatewayd/tests/support/gateway.rs crates/gatewayd/tests/support/`.
Add `pub mod serve;` to `lib.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --no-fail-fast --test serve --test serve_restart`.
Expected: it compiles; `serve` 6 fail; `serve_restart` 5 fail and 2 pass (a damaged state file
and a refused token stop `run` before anything you write is called).
- [ ] **3. Fill `mod.rs` first** (`from`, `backoff`, `sleep_unless`, `connected`), **then
`handle.rs`** (`now_ms`, `post`, `tracked`, `finished`, `typing`, `start`, `handle_post`,
`catch_up_channel`). `cargo check -p gatewayd` after each function.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test serve --test serve_restart`, five times.
Expected: 6 and 7 passed each time, in about 2 s.
- [ ] **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
- Both suites pass five times running; `make gate` prints `gate: ok`.
## Stop and report if
- A test passes only sometimes, or a test takes 5 s or more (that is a wait that timed out, not a
pass).
- A written function (`run`, `connect`, `Gateway::new`, `event_loop`, `catch_up`) seems to need a
change. They are given; report instead.