Files
boxmaker/docs/plans/M4a/14-gatewayd-serve.md
T
kyleandClaude Opus 5.5 02d5e25046 M4a task 14: post's comment was now_ms's; fixed, and every comment read
A script that rewrote the skeleton's comments put `now_ms`'s text on `post`. Task 14's second
session saw the contradiction and deliberated until cut off. Both comments are fixed, each
todo!() comment in tasks 14 and 15 was read beside its signature, and both tasks now say to stop
and quote a comment that does not fit. Lesson T29.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 00:40:05 -07:00

7.3 KiB

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 comment above a todo!() does not fit its function, or contradicts a test. The comments were written by hand and one was wrong before; stop at once and quote it, rather than guess.
  • 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.