From e87078a8ec2c591cde5bc164bda24cbef3c01dda Mon Sep 17 00:00:00 2001 From: fluid-chey Date: Wed, 4 Nov 2026 13:03:09 -0700 Subject: [PATCH] M9: leaving a live session sends what was waiting its turn Co-Authored-By: Claude Opus 5.5 --- crates/collab-net/src/driver.rs | 6 ++++++ crates/collab-net/src/driver_tests.rs | 15 +++++++++++++++ crates/collab-net/src/guest.rs | 2 ++ docs/collaboration-parity.md | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/collab-net/src/driver.rs b/crates/collab-net/src/driver.rs index d96650371..622b6c52d 100644 --- a/crates/collab-net/src/driver.rs +++ b/crates/collab-net/src/driver.rs @@ -339,6 +339,12 @@ impl Driver { /// Leave the session. pub(crate) fn leave(&mut self, now: Instant) { + // What you did before leaving still reaches the others, whatever its turn. + while let Some((_, msg)) = self.paced.pop_front() { + if !self.write(&msg) { + break; + } + } self.end(Outcome::Left, now); } diff --git a/crates/collab-net/src/driver_tests.rs b/crates/collab-net/src/driver_tests.rs index df4d5c492..83684db7e 100644 --- a/crates/collab-net/src/driver_tests.rs +++ b/crates/collab-net/src/driver_tests.rs @@ -392,6 +392,21 @@ fn a_long_queue_goes_out_paced_and_in_order() { assert_eq!(d.deadline().unwrap() - now, ms(500), "nothing left to send but the reconnect"); } +#[test] +fn leaving_sends_what_was_waiting_its_turn() { + let t0 = Instant::now(); + let (mut d, _inbox) = start(Script::default(), t0); + d.opened(t0); + d.message(&welcome(), t0); + for seq in 0..300 { + d.send(ClientMsg::Ack { seq }, t0); + } + d.leave(t0); + let seqs: Vec = d.sock.sent.iter().filter_map(|m| if let ClientMsg::Ack { seq } = m { Some(*seq) } else { None }).collect(); + assert_eq!(seqs, (0..300).collect::>()); + assert!(d.done()); +} + #[test] fn a_first_join_that_cant_reach_the_host_fails_at_once() { let t0 = Instant::now(); diff --git a/crates/collab-net/src/guest.rs b/crates/collab-net/src/guest.rs index 02685339f..f338fcce2 100644 --- a/crates/collab-net/src/guest.rs +++ b/crates/collab-net/src/guest.rs @@ -306,6 +306,8 @@ fn session( } Ok(Cmd::Lost(e, o)) if e == epoch => break o, Ok(Cmd::Leave) | Err(RecvTimeoutError::Disconnected) => { + // What you did before leaving still reaches the others, whatever its turn. + while paced.pop_front().is_some_and(|(_, m)| send(&mut writer, &m)) {} let _ = writer.close(Some(CloseFrame { code: CloseCode::Normal, reason: "".into() })); break Outcome::Left; } diff --git a/docs/collaboration-parity.md b/docs/collaboration-parity.md index 6c4aa9340..6f1faad8e 100644 --- a/docs/collaboration-parity.md +++ b/docs/collaboration-parity.md @@ -26,7 +26,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | Date | Change | Summary | |---|---|---| -| 2026-11-04 | minor | M9: a session you joined no longer turns into a local file by itself. A dropped connection reconnects until you leave (a minute's pause after each 12 attempts, a pause Retry now skips); only refusals that are for good stop it; queued messages go out paced. While disconnected the tab shows a red mark and a strip across the canvas says so; a session that ended stays on its tab with Try again, Save a copy and Dismiss, and what changed offline opens beside it when it joins again | +| 2026-11-04 | minor | M9: a session you joined no longer turns into a local file by itself. A dropped connection reconnects until you leave (a minute's pause after each 12 attempts, a pause Retry now skips); only the host ending the session and refusals that are for good stop it; queued messages go out paced. While disconnected the tab shows a red mark and a strip across the canvas says so; a session that ended stays on its tab with Try again, Save a copy and Dismiss, and what changed offline opens beside it when it joins again | | 2026-10-31 | patch | M9, #1665: the document in a live session's welcome goes compressed (deflate) to clients whose hello announces `packed`, about seven times smaller on the wire (a synthetic file of 4,040 layers: 397 KB to 53 KB); older clients keep getting it uncompressed, and the client reads a packed document only up to the size an uncompressed welcome could have had. | | 2026-10-30 | minor | M9, part of #742: a team list on `figcraft-server`. With `team_files = "edit"` (or `comment`, `view`) everyone who reaches the server sees the files shared from it now (a link of theirs has a join code: Share ▸ Start sharing) and joins them by a team list link, `figcraft://join?server=…&file=…&team=1`, which carries no secret: the server hands out the file's team link while it stays shared (`ResolveTeam`, answered like a join code), and Stop sharing revokes it with the others, disconnecting whoever joined with it. Turning the list off or changing its access refuses the old team links at once. `collab.files {refresh}` asks the server in Preferences for its list (`Admin::Team`; not the public server FigCraft ships with), and Home ▸ Online shows it below your own files: a row joins its file, its menu copies the link for the app or, with the server's `web_url`, for the browser build (`#join=…`). A browser build served beside its server names it with ``, the page's default for sharing, codes and the team list. No accounts: the list is for a server only your team reaches. Tests: `team_tests` (the server and apps over loopback), `code_tests` (links and the browser's driver), `online_tests` (the Online section), `query_tests` (the page's server). Percentages and hours unchanged | | 2026-10-30 | minor | M9, #1130: `figcraft-server` compares the SHA-256 digests of the presented and the kept server key instead of the raw strings, as `docs/server.md` says; the key file, `figcraft-server key` and rotation are unchanged. Numbers unchanged. |