From 26d41bef0de3a0d8f25b62776e966ed464a34063 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 5 Nov 2026 00:19:12 +0100 Subject: offer again ungenerated --- src/document/sync.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) (limited to 'src/document') diff --git a/src/document/sync.rs b/src/document/sync.rs index e93fa17..8265a02 100644 --- a/src/document/sync.rs +++ b/src/document/sync.rs @@ -103,6 +103,13 @@ pub(crate) struct SyncSession { /// (`withdraw_for_delta`): no more messages until it answers, or the answer is /// overdue (then the offer goes out again). pub(crate) delta_offered: bool, + /// The delta last offered: its heads (sorted, as `DeltaSpec`), and the size of the + /// message it replaced (`offer_again`). + last_offer: Option<( + Vec, + Vec, + usize, + )>, /// What the sync state knew of the peer before the last message from it /// (`answered_as_delivered`). before_receive: Option, @@ -146,6 +153,7 @@ impl SyncSession { retry_at: None, send_backoff: Backoff::new(SEND_RETRY, SEND_RETRY_MAX), delta_offered: false, + last_offer: None, before_receive: None, before_generate: (Vec::new(), false), reply: None, @@ -291,6 +299,51 @@ impl SyncSession { self.state.their_heads.clone().unwrap_or_default() } + /// We offered the delta from `from` to `to` in place of a message of `bytes`. + pub(crate) fn offered( + &mut self, + from: &[crate::document::ChangeHash], + to: &[crate::document::ChangeHash], + bytes: usize, + ) { + self.last_offer = Some((from.to_vec(), to.to_vec(), bytes)); + } + + /// The delta to offer again, if nothing changed since the last offer (`offered`): + /// the peer's heads as we know them, our `heads`, and the message too large for + /// `threshold` still. Then the message would be the same, and so the delta: its + /// `from`, offered as by `withdraw_for_delta`, without generating the message. + pub(crate) fn offer_again( + &mut self, + heads: &[crate::document::ChangeHash], + threshold: usize, + now: Instant, + ) -> Option> { + let (from, to, bytes) = self.last_offer.as_ref()?; + let sorted = |heads: &[crate::document::ChangeHash]| { + let mut heads = heads.to_vec(); + heads.sort_unstable(); + heads.dedup(); + heads + }; + let theirs = sorted(self.state.their_heads.as_deref().unwrap_or_default()); + if *bytes <= threshold || theirs != *from || sorted(heads) != *to { + return None; + } + let from = from.clone(); + if self.reply.is_none() { + self.expect_reply(Vec::new(), now, SyncBytes::default(), 0); + } + // Handed over like a generated message (`send_failed` then retries). + self.sending = Some(Sending { + sent: Vec::new(), + regenerate: false, + assumed: None, + }); + self.delta_offered = true; + Some(from) + } + /// The message just generated, withdrawn for a delta (`withdraw_for_delta`), turns /// out to carry nothing the peer lacks (it has our heads): none goes, and nothing is /// offered or awaited. @@ -783,6 +836,39 @@ mod tests { assert!(!a.delta_offered); } + /// An offer that lapsed goes again as it was while nothing changed; otherwise, or + /// once the message would fit, it is generated afresh. + #[test] + fn the_same_offer_goes_again_ungenerated() { + use crate::document::testing::change; + use automerge::{ROOT, transaction::Transactable}; + let mut ours = Document::new(); + change(&mut ours, |doc| doc.put(ROOT, "k", 1).unwrap()); + let heads = ours.get_heads(); + let mut a = session(); + a.state.their_heads = Some(Vec::new()); + let now = Instant::now(); + assert_eq!( + a.offer_again(&heads, 1000, now), + None, + "nothing offered yet" + ); + + a.offered(&[], &heads, 5000); + assert_eq!(a.offer_again(&heads, 1000, now), Some(Vec::new())); + assert!(a.delta_offered && a.reply.is_some() && a.sending.is_some()); + assert_eq!(a.offer_again(&heads, 5000, now), None, "it would fit now"); + + change(&mut ours, |doc| doc.put(ROOT, "k", 2).unwrap()); + assert_eq!( + a.offer_again(&ours.get_heads(), 1000, now), + None, + "ours moved" + ); + a.state.their_heads = Some(heads.clone()); + assert_eq!(a.offer_again(&heads, 1000, now), None, "theirs moved"); + } + /// A message too large to send is taken back: the peer, after the offer and an /// overdue answer, gets the same changes in the next message. #[test] -- cgit v1.3.1