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>
This commit is contained in:
2026-09-23 21:50:18 -07:00
co-authored by Claude Opus 5.5
parent d1d8531495
commit 564875f0a0
6 changed files with 203 additions and 64 deletions
@@ -21,58 +21,101 @@ fn now_ms() -> i64 {
impl Gateway {
/// Post in a thread; a failure is logged, not fatal.
pub(super) fn post(&self, channel: &str, root: &str, text: &str) {
// `self.client.create_post`; an error is logged, "gatewayd: cannot post in <channel>
// (thread <root>): <error>".
// `SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0)`, then
// `i64::try_from(ms).unwrap_or(i64::MAX)`.
todo!()
}
/// Is this channel one whose posts `gatewayd` keeps track of?
fn tracked(&self, channel: &str, channel_type: &str) -> bool {
// channel_type "D", or the channel is in allow.channels.
// `channel_type == "D" || self.config.allow.channels.iter().any(|c| c == channel)`.
todo!()
}
/// One new post, live or caught up.
pub(super) fn handle_post(&mut self, post: &Post, channel_type: &str) -> Result<(), Stop> {
// 1. Not tracked, or seen -> nothing. 2. `state.handled(...)?` before anything else. 3.
// Route it: NotAllowed -> log "gatewayd: ignored post <id> from <user>: not allowed"
// (never the message); other Ignore -> nothing; Reply -> post it; Queue -> join the
// thread when joins_thread, then push: Start -> `start`, Waiting -> nothing, Full ->
// post BUSY.
// 1. `if !self.tracked(&post.channel_id, channel_type) || self.state.seen(&post.id)`:
// return Ok(()).
// 2. `self.state.handled(&post.id, &post.channel_id, post.create_at)?;` before anything
// else: after a crash, a post is never answered twice.
// 3. Route it, with the state answering "is this thread ours?":
// `let state = &self.state;`
// `let route = self.router.route(post, channel_type, &|root| state.knows_thread(root));`
// then `match route`:
// - Route::Ignore(Ignored::NotAllowed): log
// "gatewayd: ignored post <post.id> from <post.user_id>: not allowed"
// (never the text).
// - Route::Ignore(_): nothing.
// - Route::Reply { thread, text }: `self.post(&thread.channel, &thread.root, &text)`.
// - Route::Queue(message): if `message.joins_thread`,
// `self.state.join_thread(&message.thread.root)?`; then
// `match self.queues.push(message)`:
// Pushed::Start(batch) -> `self.start(batch)?`; Pushed::Waiting -> nothing;
// Pushed::Full(thread) -> `self.post(&thread.channel, &thread.root, BUSY)`.
// 4. Ok(()).
todo!()
}
/// Record the turn as in flight and run it on its own thread.
fn start(&mut self, batch: Batch) -> Result<(), Stop> {
// `state.start_turn` (session, channel, root). Spawn with std::thread::Builder: `deliver`
// with the client, the loop socket, the batch and the log, then send the session on
// done_tx. If spawning fails: log "gatewayd: cannot start a thread for <session>: <e>",
// post LOOP_DOWN in the thread, and send the session on done_tx.
// 1. `self.state.start_turn(InFlight { session: batch.session.as_str().to_string(),
// channel: batch.thread.channel.clone(), root: batch.thread.root.clone() })?;`
// 2. Clone what the thread takes, before the `move`:
// `let (client, socket, log, done) = (Arc::clone(&self.client),
// self.loop_socket.clone(), Arc::clone(&self.log), self.done_tx.clone());`
// `let (session, thread) = (batch.session.clone(), batch.thread.clone());`
// 3. `let spawned = std::thread::Builder::new().spawn(move || {
// deliver(client.as_ref(), &socket, &batch, &|line| log(line));
// let _ = done.send(batch.session); });`
// 4. `if let Err(e) = spawned`: log "gatewayd: cannot start a thread for <session>: <e>"
// (`session.as_str()`), `self.post(&thread.channel, &thread.root, LOOP_DOWN)`, and
// `let _ = self.done_tx.send(session);` so the session does not stay busy.
// 5. Ok(()).
todo!()
}
/// Turns that ended: out of flight, and the next batch of each session started.
pub(super) fn finished(&mut self) -> Result<(), Stop> {
// For each session on done_rx (try_recv, never blocking): `end_turn`, then `queues.finish`;
// a batch it returns is started.
// `while let Ok(session) = self.done_rx.try_recv() {` (never blocks)
// `self.state.end_turn(session.as_str())?;`
// `if let Some(batch) = self.queues.finish(&session) { self.start(batch)?; }` `}`
// Then Ok(()).
todo!()
}
/// Show this bot as typing in every thread with a turn running.
pub(super) fn typing(&mut self, ws: &mut Ws) -> Result<(), WsError> {
// For each of `queues.threads()`: seq += 1, send `typing(seq, channel, root)` as text.
// `for thread in self.queues.threads() { self.seq += 1;
// ws.send_text(&typing(self.seq, &thread.channel, &thread.root))?; }` then Ok(()).
todo!()
}
/// Answer what arrived while `gatewayd` was away: the direct channel with each allowed user,
/// and each allowed channel. A channel seen for the first time starts from now.
/// and each allowed channel. Written for you: it is the glue.
pub(super) fn catch_up(&mut self) -> Result<(), Stop> {
// Channels: the direct channel with each allowed user ("D"; an error is logged "gatewayd:
// no direct channel with <user>: <e>" and skipped), then each allowed channel ("O"). For
// each: no mark -> `mark(channel, now_ms())` and skip; else `posts_since(channel, mark)`
// (an error is logged "gatewayd: cannot catch up <channel>: <e>" and skipped); when full,
// log "gatewayd: <channel>: too many posts to catch up; some may be missed"; `handle_post`
// each post in order.
let mut channels = Vec::new();
for user in self.config.allow.users.clone() {
match self.client.direct_channel(&self.me.id, &user) {
Ok(id) => channels.push((id, "D")),
Err(e) => (self.log)(&format!("gatewayd: no direct channel with {user}: {e}")),
}
}
channels.extend(self.config.allow.channels.iter().map(|c| (c.clone(), "O")));
for (channel, channel_type) in channels {
self.catch_up_channel(&channel, channel_type)?;
}
Ok(())
}
/// One channel's posts since its mark. A channel seen for the first time starts from now.
fn catch_up_channel(&mut self, channel: &str, channel_type: &str) -> Result<(), Stop> {
// 1. `let Some(since) = self.state.since(channel) else { ... }`: with no mark,
// `self.state.mark(channel, now_ms())?` and return Ok(()).
// 2. `self.client.posts_since(channel, since)`: an Err(e) is logged
// "gatewayd: cannot catch up <channel>: <e>" and returns Ok(()).
// 3. When `found.full`, log "gatewayd: <channel>: too many posts to catch up; some may be
// missed".
// 4. `for post in &found.posts { self.handle_post(post, channel_type)?; }` and Ok(()).
todo!()
}
}