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 ff13dce70..a08a48b92 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 | +| 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 | | 2026-11-04 | minor | M9: joining a session again merges what you did while disconnected as one undoable edit, through the three-way merge branches use, as the same client when the server still knows it. What conflicts keeps the other person's, and your whole version opens beside it as an unsaved copy. A server that no longer knows you merges the same way instead of discarding unconfirmed work | | 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 |