aboutsummaryrefslogtreecommitdiffstats
path: root/src/document
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 04:50:17 +0100
committertv <tv@krebsco.de>2026-11-05 04:50:17 +0100
commit784c6e7c9088dde2f23cc1d02381a0c5fe8b010b (patch)
treef4a0928bf6e5279f6146228e97dd51e5db250cce /src/document
parentc7226385f3765b2a72b7b4d95129fb20fec58d5c (diff)
pushed heads as tips
Diffstat (limited to 'src/document')
-rw-r--r--src/document/sync.rs66
1 files changed, 60 insertions, 6 deletions
diff --git a/src/document/sync.rs b/src/document/sync.rs
index e7a2665..9415bd0 100644
--- a/src/document/sync.rs
+++ b/src/document/sync.rs
@@ -6,6 +6,7 @@
//! forgetting what was sent when the peer still needs it) live here with it, so an
//! Automerge upgrade that changes the state touches this file only.
+use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
@@ -22,6 +23,29 @@ use crate::transport::SyncPayload;
/// A sync message from a peer, decoded (`decode`), to apply (`SyncSession::receive`).
pub(crate) struct Received(sync::Message);
+/// The changes none of the others depend on, sorted as heads are (`get_heads`).
+fn tips<'a>(
+ changes: impl Iterator<
+ Item = (
+ crate::document::ChangeHash,
+ &'a [crate::document::ChangeHash],
+ ),
+ >,
+) -> Vec<crate::document::ChangeHash> {
+ let changes: Vec<_> = changes.collect();
+ let depended: HashSet<crate::document::ChangeHash> = changes
+ .iter()
+ .flat_map(|(_, deps)| deps.iter().copied())
+ .collect();
+ let mut tips: Vec<crate::document::ChangeHash> = changes
+ .into_iter()
+ .map(|(hash, _)| hash)
+ .filter(|hash| !depended.contains(hash))
+ .collect();
+ tips.sort_unstable();
+ tips
+}
+
impl Received {
/// Whether it carries changes (it may carry only heads and what the peer needs).
pub(crate) fn carries_changes(&self) -> bool {
@@ -32,11 +56,23 @@ impl Received {
/// message that would have brought them: the sender's `heads`, nothing needed, and
/// as what it has, everything we had (`ours`, our heads before: it takes the
/// message as delivered, so it had all of them).
+ ///
+ /// No heads stand for the tips of the changes (`SyncSession::push`). Changes that
+ /// don't decode leave the heads out; applying them fails as it would have anyway.
pub(crate) fn pushed(
heads: Vec<crate::document::ChangeHash>,
changes: Vec<Vec<u8>>,
ours: Vec<crate::document::ChangeHash>,
) -> Self {
+ let heads = if heads.is_empty() {
+ let decoded: Vec<automerge::Change> = changes
+ .iter()
+ .filter_map(|bytes| automerge::Change::try_from(bytes.as_slice()).ok())
+ .collect();
+ tips(decoded.iter().map(|change| (change.hash(), change.deps())))
+ } else {
+ heads
+ };
Self(sync::Message {
heads,
need: Vec::new(),
@@ -352,8 +388,11 @@ impl SyncSession {
regenerate: false,
assumed: None,
});
+ // Our heads are mostly the tips of the changes pushed: then they go as none, and
+ // the receiver finds them in the changes (`Received::pushed`).
+ let tips = tips(changes.iter().map(|change| (change.hash(), change.deps())));
Some((
- heads,
+ if tips == heads { Vec::new() } else { heads },
changes
.iter()
.map(|change| change.raw_bytes().to_vec())
@@ -1453,16 +1492,18 @@ mod tests {
change(&mut ours, |doc| doc.put(ROOT, "news", 2).unwrap());
let (heads, changes) = a.push(&ours, 1 << 20).expect("news to push");
assert_eq!(changes.len(), 1);
+ assert!(
+ heads.is_empty(),
+ "our heads are the change's: they go as none"
+ );
a.assume_delivered(&ours.get_heads());
a.send_succeeded(a.id());
assert!(a.push(&ours, 1 << 20).is_none(), "sent already");
let before = theirs.get_heads();
- b.receive(
- &mut theirs,
- Received::pushed(heads, changes, before.clone()),
- )
- .unwrap();
+ let message = Received::pushed(heads, changes, before.clone());
+ assert_eq!(message.heads(), ours.get_heads(), "found in the changes");
+ b.receive(&mut theirs, message).unwrap();
b.came_from_peer(&theirs, &before);
assert_eq!(theirs.get_heads(), ours.get_heads());
assert!(b.answered_as_delivered(&theirs, &theirs.get_heads()));
@@ -1472,6 +1513,19 @@ mod tests {
);
}
+ /// Tips are the changes none of the others depend on, sorted as heads are.
+ #[test]
+ fn tips_are_what_nothing_depends_on() {
+ let hash = |n: u8| crate::document::ChangeHash([n; 32]);
+ let (a, b, c, d) = (hash(1), hash(2), hash(3), hash(4));
+ let deps = [vec![], vec![a], vec![a], vec![b, c, hash(9)]];
+ let all = [a, b, c, d];
+ let tips_of = |which: &[usize]| tips(which.iter().map(|&i| (all[i], deps[i].as_slice())));
+ assert_eq!(tips_of(&[0, 1, 2, 3]), vec![d]);
+ assert_eq!(tips_of(&[2, 0, 1]), vec![b, c]);
+ assert_eq!(tips_of(&[]), Vec::new());
+ }
+
/// Changes larger than the limit aren't pushed: they go as `generate` sends them.
#[test]
fn large_changes_are_not_pushed() {