aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/delta.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/delta.rs')
-rw-r--r--src/remake/delta.rs504
1 files changed, 379 insertions, 125 deletions
diff --git a/src/remake/delta.rs b/src/remake/delta.rs
index 80e5443..638231c 100644
--- a/src/remake/delta.rs
+++ b/src/remake/delta.rs
@@ -20,15 +20,23 @@
//!
//! The protocol's own metadata (`_continuum/remakes`, `_continuum/epochs`) is never
//! replayed: the snapshot owns it.
+//!
+//! Last writer wins, by the writers' clocks ([`replay_newest`]): a replayed value is a
+//! fresh operation, which by Automerge's order beats everything the document it goes
+//! into has, newer writes included. So a path is replayed only if no change in that
+//! document touched it (or a path above or below it) later than the write being
+//! replayed, as the changes' timestamps say; and the replayed changes carry the times
+//! of the writes they replay, not of the replay, so later replays judge by those.
-use std::collections::{BTreeMap, HashSet};
+use std::collections::{BTreeMap, HashMap, HashSet};
-use automerge::{ActorId, AutoCommit, ChangeHash, PatchAction, Prop};
-use tracing::warn;
+use crate::document::{ActorId, ChangeHash, Document, ReadDoc, Transactable};
+use serde::{Deserialize, Serialize};
+use tracing::{info, warn};
use super::kind::{Translation, Translator};
use crate::document::meta::{self, EPOCHS, REMAKES, VOTES};
-use crate::document::store::{self, StoreError};
+use crate::document::store::{self, InLists, StoreError};
#[derive(Debug, Default, Clone)]
pub(crate) struct ReplayStats {
@@ -36,6 +44,10 @@ pub(crate) struct ReplayStats {
pub(crate) failed: usize,
/// Paths the remake declared obsolete.
pub(crate) dropped: usize,
+ /// Paths not replayed because a newer write touched them ([`replay_newest`]), and
+ /// left out (`Superseded::Drop`; with `Superseded::Quarantine`, they are among
+ /// `quarantined`).
+ pub(crate) superseded: usize,
/// Paths the remake had no place for, with the reason.
pub(crate) quarantined: Vec<(Vec<String>, String)>,
}
@@ -46,17 +58,148 @@ impl ReplayStats {
self.replayed += other.replayed;
self.failed += other.failed;
self.dropped += other.dropped;
+ self.superseded += other.superseded;
self.quarantined.extend(other.quarantined);
}
}
+/// What a replay does with a write that a newer one superseded (`--superseded-writes`):
+/// leave it out, logged (last writer wins), or put it in the quarantine, where an
+/// operator can still replay it.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) enum Superseded {
+ #[default]
+ Drop,
+ Quarantine,
+}
+
+/// Replays into `dst` what changed in `src` between `before` and `after` (both in
+/// `src`), last writer wins: a path whose newest write there (by its change's
+/// timestamp) is older than a change in `dst` touching it, or a path above or below it,
+/// stays as `dst` has it (`superseded` says what becomes of the write). Ties go to the
+/// replay. The replayed paths are committed grouped by the time of the writes they
+/// replay, each change stamped with it.
+pub(crate) fn replay_newest(
+ src: &Document,
+ before: &[ChangeHash],
+ after: &[ChangeHash],
+ dst: &mut Document,
+ translator: &Translator,
+ superseded: Superseded,
+) -> ReplayStats {
+ let mut stats = ReplayStats::default();
+ if before == after {
+ return stats;
+ }
+ let paths = changed_paths(src, before, after);
+ let written = touched(src, &between(src, before, after));
+ let ours: Vec<i64> = paths
+ .iter()
+ .map(|path| newest(&written, path).unwrap_or(0))
+ .collect();
+ // Only changes newer than the oldest write replayed can supersede one; the
+ // snapshot (time 0) never does.
+ let oldest = ours.iter().copied().min().unwrap_or(0);
+ let newer: Vec<ChangeHash> = dst
+ .get_changes(&[])
+ .iter()
+ .filter(|change| change.timestamp() > oldest)
+ .map(|change| change.hash())
+ .collect();
+ let theirs = touched(dst, &newer);
+
+ let mut by_time: BTreeMap<i64, Vec<Vec<String>>> = BTreeMap::new();
+ for (path, at) in paths.into_iter().zip(ours) {
+ match newest(&theirs, &path) {
+ Some(newer) if newer > at => {
+ stats.superseded += 1;
+ if superseded == Superseded::Quarantine {
+ stats.quarantined.push((
+ path,
+ format!("a newer write (at {newer}, this one at {at}) superseded it"),
+ ));
+ }
+ }
+ _ => by_time.entry(at).or_default().push(path),
+ }
+ }
+ for (at, paths) in by_time {
+ let mut transaction = dst.transaction();
+ stats.absorb(replay_paths(src, &mut transaction, &paths, translator));
+ transaction.commit_with(automerge::transaction::CommitOptions::default().with_time(at));
+ }
+ if stats.superseded > 0 {
+ info!(
+ superseded = stats.superseded,
+ quarantined = superseded == Superseded::Quarantine,
+ "Writes not replayed: newer ones touched their paths"
+ );
+ }
+ if superseded == Superseded::Quarantine {
+ // Counted as quarantined, not as left out.
+ stats.superseded = 0;
+ }
+ stats
+}
+
+/// The changes of `doc` after `before` up to `after`: those `after` builds on that
+/// `before` doesn't.
+fn between(doc: &Document, before: &[ChangeHash], after: &[ChangeHash]) -> Vec<ChangeHash> {
+ let deps: HashMap<ChangeHash, Vec<ChangeHash>> = doc
+ .get_changes(before)
+ .iter()
+ .map(|change| (change.hash(), change.deps().to_vec()))
+ .collect();
+ let mut found = HashSet::new();
+ let mut todo: Vec<ChangeHash> = after.to_vec();
+ while let Some(hash) = todo.pop() {
+ if let Some(parents) = deps.get(&hash)
+ && found.insert(hash)
+ {
+ todo.extend(parents.iter().copied());
+ }
+ }
+ found.into_iter().collect()
+}
+
+/// For each of `changes` (in `doc`), its time and the store paths it touched, from its
+/// operations (`store::changed_by`: a diff per change would cost what the maps it
+/// touched hold, `remake::bench`).
+fn touched(doc: &Document, changes: &[ChangeHash]) -> Vec<(i64, Vec<Vec<String>>)> {
+ changes
+ .iter()
+ .filter_map(|hash| {
+ let change = doc.get_change_by_hash(hash)?;
+ let paths = store::changed_by(doc, hash, InLists::Whole)?;
+ Some((change.timestamp(), paths))
+ })
+ .collect()
+}
+
+/// The time of the newest of `changes` that touched `path`, a path above it or one
+/// below it.
+fn newest(changes: &[(i64, Vec<Vec<String>>)], path: &[String]) -> Option<i64> {
+ changes
+ .iter()
+ .filter(|(_, paths)| {
+ paths
+ .iter()
+ .any(|other| other.starts_with(path) || path.starts_with(other))
+ })
+ .map(|(at, _)| *at)
+ .max()
+}
+
/// Replays into `dst` everything that changed in `src` between `before` and `after`,
-/// as one commit. Both head sets must be in `src`.
+/// as one commit, whatever `dst` has (no last writer wins): what a replay copies, for
+/// tests. Both head sets must be in `src`.
+#[cfg(test)]
pub(crate) fn replay(
- src: &mut AutoCommit,
+ src: &Document,
before: &[ChangeHash],
after: &[ChangeHash],
- dst: &mut AutoCommit,
+ dst: &mut Document,
translator: &Translator,
) -> ReplayStats {
if before == after {
@@ -64,14 +207,17 @@ pub(crate) fn replay(
}
let paths = changed_paths(src, before, after);
- replay_paths(src, dst, &paths, translator)
+ let mut transaction = dst.transaction();
+ let stats = replay_paths(src, &mut transaction, &paths, translator);
+ transaction.commit();
+ stats
}
-/// Replays the state of `paths` (from [`changed_paths`]) from `src` into `dst`, as one
-/// commit. Lets callers find the paths outside a lock and copy them inside it.
+/// Replays the state of `paths` (from [`changed_paths`]) from `src` into `dst` (the
+/// caller commits). Lets callers find the paths outside a lock and copy them inside it.
pub(crate) fn replay_paths(
- src: &AutoCommit,
- dst: &mut AutoCommit,
+ src: &Document,
+ dst: &mut impl Transactable,
paths: &[Vec<String>],
translator: &Translator,
) -> ReplayStats {
@@ -79,13 +225,12 @@ pub(crate) fn replay_paths(
for path in paths {
replay_path(src, dst, path, translator, &mut stats);
}
- let _ = dst.commit();
stats
}
fn replay_path(
- src: &AutoCommit,
- dst: &mut AutoCommit,
+ src: &Document,
+ dst: &mut impl Transactable,
path: &[String],
translator: &Translator,
stats: &mut ReplayStats,
@@ -164,7 +309,7 @@ pub(crate) struct Missing {
/// replayed by the nodes that switched, and replaying them again would overwrite newer
/// writes in the new epoch with stale ones.
pub(crate) fn missing_from_snapshot(
- doc: &mut AutoCommit,
+ doc: &Document,
clock: &BTreeMap<String, u64>,
own: &[ActorId],
) -> Missing {
@@ -225,11 +370,14 @@ pub(crate) fn missing_from_snapshot(
/// The minimal set of store paths covering every change between `before` and `after`:
/// sorted, and no path is a prefix of another.
pub(crate) fn changed_paths(
- doc: &mut AutoCommit,
+ doc: &Document,
before: &[ChangeHash],
after: &[ChangeHash],
) -> Vec<Vec<String>> {
- let mut paths: Vec<Vec<String>> = doc.diff(before, after).iter().map(patch_path).collect();
+ let mut paths: Vec<Vec<String>> = store::changed(doc, before, after, InLists::Whole)
+ .into_iter()
+ .map(|changed| changed.path)
+ .collect();
paths.sort();
paths.dedup();
@@ -245,49 +393,26 @@ pub(crate) fn changed_paths(
minimal
}
-fn patch_path(patch: &automerge::Patch) -> Vec<String> {
- let mut path = Vec::new();
- for (_, prop) in &patch.path {
- match prop {
- Prop::Map(key) => path.push(key.clone()),
- // Inside a list: the list is replayed as a whole.
- Prop::Seq(_) => return path,
- }
- }
-
- match &patch.action {
- PatchAction::PutMap { key, .. } | PatchAction::DeleteMap { key, .. } => {
- path.push(key.clone())
- }
- PatchAction::Increment {
- prop: Prop::Map(key),
- ..
- } => path.push(key.clone()),
- // List and text edits: the containing object.
- _ => {}
- }
- path
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::{change, try_change};
use automerge::{ObjType, ROOT, ReadDoc, ScalarValue, transaction::Transactable};
use serde_json::json;
- fn view(doc: &AutoCommit) -> serde_json::Value {
+ fn view(doc: &Document) -> serde_json::Value {
store::read_path(doc, &[]).unwrap().unwrap()
}
/// A random store write as the API makes it (one change): a scalar or a small map
/// at a path up to three keys deep, or a delete. Writes the store refuses (e.g. into
/// a scalar) are rolled back.
- fn random_write(rng: &mut crate::testrand::Rng, doc: &mut AutoCommit) {
+ fn random_write(rng: &mut crate::testrand::Rng, doc: &mut Document) {
const KEYS: [&str; 3] = ["a", "b", "c"];
let depth = rng.between(1, 3);
let path: Vec<String> = (0..depth).map(|_| rng.pick(&KEYS).to_string()).collect();
- let result = if rng.chance(0.2) {
- store::delete_path(doc, &path, false)
+ let _ = if rng.chance(0.2) {
+ try_change(doc, |doc| store::delete_path(doc, &path, false))
} else {
let value = match rng.below(4) {
0 => json!(rng.below(100)),
@@ -299,16 +424,8 @@ mod tests {
serde_json::Value::Object(map)
}
};
- store::write_path(doc, &path, &value)
+ try_change(doc, |doc| store::write_path(doc, &path, &value))
};
- match result {
- Ok(()) => {
- doc.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
- }
- Err(_) => {
- doc.rollback();
- }
- }
}
/// Compaction's promise: a snapshot of the base plus everything replayed since gives
@@ -317,16 +434,17 @@ mod tests {
fn replay_reproduces_the_document() {
crate::testrand::for_seeds(200, |rng| {
let actor = |i: u8| ActorId::from(&[i; 16][..]);
- let mut old = AutoCommit::new().with_actor(actor(0));
+ let mut old = Document::new().with_actor(actor(0));
for _ in 0..rng.between(0, 8) {
random_write(rng, &mut old);
}
let base = old.get_heads();
- let mut new = AutoCommit::new().with_actor(actor(100));
- store::materialize_into(&old, &mut new, &|_| false).unwrap();
- new.commit();
+ let mut new = Document::new().with_actor(actor(100));
+ change(&mut new, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap()
+ });
- let mut nodes: Vec<AutoCommit> = (1..=rng.between(1, 3))
+ let mut nodes: Vec<Document> = (1..=rng.between(1, 3))
.map(|i| old.fork().with_actor(actor(i as u8)))
.collect();
for _ in 0..rng.between(1, 20) {
@@ -338,7 +456,7 @@ mod tests {
}
let after = old.get_heads();
- let stats = replay(&mut old, &base, &after, &mut new, &Translator::default());
+ let stats = replay(&old, &base, &after, &mut new, &Translator::default());
assert_eq!(stats.failed, 0);
assert_eq!(view(&new), view(&old));
});
@@ -369,12 +487,12 @@ mod tests {
fn changed_paths_cover_every_change_minimally() {
crate::testrand::for_seeds(300, |rng| {
let actor = |i: u8| ActorId::from(&[i; 16][..]);
- let mut doc = AutoCommit::new().with_actor(actor(0));
+ let mut doc = Document::new().with_actor(actor(0));
for _ in 0..rng.between(0, 8) {
random_write(rng, &mut doc);
}
let before = doc.get_heads();
- let mut forks: Vec<AutoCommit> = (1..=rng.between(1, 3))
+ let mut forks: Vec<Document> = (1..=rng.between(1, 3))
.map(|i| doc.fork().with_actor(actor(i as u8)))
.collect();
for _ in 0..rng.between(0, 12) {
@@ -386,7 +504,7 @@ mod tests {
}
let after = doc.get_heads();
- let paths = changed_paths(&mut doc, &before, &after);
+ let paths = changed_paths(&doc, &before, &after);
for pair in paths.windows(2) {
assert!(pair[0] < pair[1], "not sorted: {paths:?}");
assert!(!pair[1].starts_with(&pair[0]), "not minimal: {paths:?}");
@@ -415,23 +533,25 @@ mod tests {
#[test]
fn minimal_paths() {
- let mut doc = AutoCommit::new();
- doc.put(ROOT, "untouched", 1_i64).unwrap();
+ let mut doc = Document::new();
+ change(&mut doc, |doc| doc.put(ROOT, "untouched", 1_i64).unwrap());
let before = doc.get_heads();
- let m = doc.put_object(ROOT, "m", ObjType::Map).unwrap();
- doc.put(&m, "a", 1_i64).unwrap();
- doc.put(ROOT, "x", 1_i64).unwrap();
- doc.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ let m = doc.put_object(ROOT, "m", ObjType::Map).unwrap();
+ doc.put(&m, "a", 1_i64).unwrap();
+ doc.put(ROOT, "x", 1_i64).unwrap();
+ doc.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
+ });
let middle = doc.get_heads();
- doc.increment(ROOT, "c", 2).unwrap();
- let list = doc.put_object(ROOT, "l", ObjType::List).unwrap();
- doc.insert(&list, 0, "e").unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ doc.increment(ROOT, "c", 2).unwrap();
+ let list = doc.put_object(ROOT, "l", ObjType::List).unwrap();
+ doc.insert(&list, 0, "e").unwrap();
+ });
let after = doc.get_heads();
- let all = changed_paths(&mut doc, &before, &after);
+ let all = changed_paths(&doc, &before, &after);
assert_eq!(
all,
vec![
@@ -442,37 +562,176 @@ mod tests {
]
);
- let later = changed_paths(&mut doc, &middle, &after);
+ let later = changed_paths(&doc, &middle, &after);
assert_eq!(later, vec![vec!["c".to_owned()], vec!["l".to_owned()]]);
}
+ /// Writes to `doc` as one change made at `at` (Unix ms).
+ fn write_at(
+ doc: &mut Document,
+ at: i64,
+ f: impl FnOnce(&mut automerge::transaction::Transaction<'_>),
+ ) {
+ let mut transaction = doc.transaction();
+ f(&mut transaction);
+ transaction.commit_with(automerge::transaction::CommitOptions::default().with_time(at));
+ }
+
+ /// An old document with `x`, `m/a` and `m/b` written at 10 after the base, and a new
+ /// one holding the base; `newer` writes to the new one first.
+ fn superseding(
+ newer: impl FnOnce(&mut Document),
+ superseded: Superseded,
+ ) -> (Document, ReplayStats) {
+ let mut old = Document::new();
+ write_at(&mut old, 1, |old| {
+ let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
+ old.put(&m, "a", "base").unwrap();
+ });
+ let base = old.get_heads();
+ let mut new = Document::new();
+ write_at(&mut new, 0, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap();
+ });
+ newer(&mut new);
+ write_at(&mut old, 10, |old| {
+ old.put(ROOT, "x", "old").unwrap();
+ let (_, m) = old.get(ROOT, "m").unwrap().unwrap();
+ old.put(&m, "a", "old").unwrap();
+ old.put(&m, "b", "old").unwrap();
+ });
+ let after = old.get_heads();
+ let stats = replay_newest(
+ &old,
+ &base,
+ &after,
+ &mut new,
+ &Translator::default(),
+ superseded,
+ );
+ (new, stats)
+ }
+
+ #[test]
+ fn replay_newest_keeps_newer_writes() {
+ // Nothing newer: everything replayed, at the time it was written.
+ let (new, stats) = superseding(|_| {}, Superseded::Drop);
+ assert_eq!((stats.replayed, stats.superseded), (3, 0));
+ assert_eq!(
+ view(&new),
+ json!({ "x": "old", "m": { "a": "old", "b": "old" } })
+ );
+ let times: Vec<i64> = new.get_changes(&[]).iter().map(|c| c.timestamp()).collect();
+ assert_eq!(times, [0, 10]);
+
+ // A newer write at the path wins; an older one doesn't.
+ let (new, stats) = superseding(
+ |new| write_at(new, 20, |new| new.put(ROOT, "x", "newer").unwrap()),
+ Superseded::Drop,
+ );
+ assert_eq!((stats.replayed, stats.superseded), (2, 1));
+ assert_eq!(view(&new)["x"], json!("newer"));
+ assert_eq!(view(&new)["m"], json!({ "a": "old", "b": "old" }));
+ let (new, _) = superseding(
+ |new| write_at(new, 5, |new| new.put(ROOT, "x", "older").unwrap()),
+ Superseded::Drop,
+ );
+ assert_eq!(view(&new)["x"], json!("old"));
+
+ // Per key: a newer `m/a` wins over the replayed `m/a`, not over `m/b`.
+ let (new, stats) = superseding(
+ |new| {
+ write_at(new, 20, |new| {
+ let (_, m) = new.get(ROOT, "m").unwrap().unwrap();
+ new.put(&m, "a", "newer").unwrap();
+ })
+ },
+ Superseded::Drop,
+ );
+ assert_eq!(stats.superseded, 1);
+ assert_eq!(view(&new)["m"], json!({ "a": "newer", "b": "old" }));
+ // A newer write above the paths (`m` replaced) wins over both.
+ let (new, stats) = superseding(
+ |new| write_at(new, 20, |new| new.put(ROOT, "m", "a scalar now").unwrap()),
+ Superseded::Drop,
+ );
+ assert_eq!(stats.superseded, 2);
+ assert_eq!(view(&new)["m"], json!("a scalar now"));
+
+ // Quarantined instead, with the time of each.
+ let (_, stats) = superseding(
+ |new| write_at(new, 20, |new| new.put(ROOT, "x", "newer").unwrap()),
+ Superseded::Quarantine,
+ );
+ assert_eq!(stats.superseded, 0);
+ assert_eq!(stats.quarantined.len(), 1);
+ assert_eq!(stats.quarantined[0].0, ["x"]);
+ }
+
+ /// A newer write below a replayed path wins too: the replay would replace it.
+ #[test]
+ fn replay_newest_keeps_newer_writes_below() {
+ let mut old = Document::new();
+ write_at(&mut old, 1, |old| old.put(ROOT, "base", true).unwrap());
+ let base = old.get_heads();
+ let mut new = Document::new();
+ write_at(&mut new, 0, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap();
+ new.put_object(ROOT, "m", ObjType::Map).unwrap();
+ });
+ write_at(&mut new, 20, |new| {
+ let (_, m) = new.get(ROOT, "m").unwrap().unwrap();
+ new.put(&m, "a", "newer").unwrap();
+ });
+ // The whole map, older.
+ write_at(&mut old, 10, |old| {
+ let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
+ old.put(&m, "a", "old").unwrap();
+ });
+ let after = old.get_heads();
+ let stats = replay_newest(
+ &old,
+ &base,
+ &after,
+ &mut new,
+ &Translator::default(),
+ Superseded::Drop,
+ );
+ assert_eq!(stats.superseded, 1);
+ assert_eq!(view(&new)["m"], json!({ "a": "newer" }));
+ }
+
#[test]
fn replays_state_per_key() {
// Base shared by the old document and the snapshot.
- let mut old = AutoCommit::new();
- let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
- old.put(&m, "keep", "base").unwrap();
- old.put(&m, "change", "base").unwrap();
- old.put(ROOT, "gone", true).unwrap();
- old.commit();
+ let mut old = Document::new();
+ let m = change(&mut old, |old| {
+ let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
+ old.put(&m, "keep", "base").unwrap();
+ old.put(&m, "change", "base").unwrap();
+ old.put(ROOT, "gone", true).unwrap();
+ m
+ });
let base = old.get_heads();
- let mut new = AutoCommit::new();
- store::materialize_into(&old, &mut new, &|_| false).unwrap();
- // Something the new epoch already holds that the delta must not clobber.
- let Some((_, m_new)) = new.get(ROOT, "m").unwrap() else {
- panic!("m missing");
- };
- new.put(&m_new, "keep", "new epoch").unwrap();
- new.commit();
+ let mut new = Document::new();
+ change(&mut new, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap();
+ // Something the new epoch already holds that the delta must not clobber.
+ let Some((_, m_new)) = new.get(ROOT, "m").unwrap() else {
+ panic!("m missing");
+ };
+ new.put(&m_new, "keep", "new epoch").unwrap();
+ });
// The delta: one key changed, one key deleted.
- old.put(&m, "change", "delta").unwrap();
- old.delete(ROOT, "gone").unwrap();
- old.commit();
+ change(&mut old, |old| {
+ old.put(&m, "change", "delta").unwrap();
+ old.delete(ROOT, "gone").unwrap();
+ });
let after = old.get_heads();
- let stats = replay(&mut old, &base, &after, &mut new, &Translator::default());
+ let stats = replay(&old, &base, &after, &mut new, &Translator::default());
assert_eq!(stats.failed, 0);
assert_eq!(
view(&new),
@@ -485,9 +744,8 @@ mod tests {
let own = ActorId::from(&b"own-actor"[..]);
let other = ActorId::from(&b"other-actor"[..]);
- let mut doc = AutoCommit::new().with_actor(own.clone());
- doc.put(ROOT, "base", 1_i64).unwrap();
- doc.commit();
+ let mut doc = Document::new().with_actor(own.clone());
+ change(&mut doc, |doc| doc.put(ROOT, "base", 1_i64).unwrap());
let base = doc.get_heads();
// The snapshot contains the base only.
@@ -495,28 +753,28 @@ mod tests {
// A foreign change after the base (replayed by its own node) ...
let mut foreign = doc.fork().with_actor(other);
- foreign.put(ROOT, "theirs", 1_i64).unwrap();
- foreign.commit();
+ change(&mut foreign, |foreign| {
+ foreign.put(ROOT, "theirs", 1_i64).unwrap()
+ });
doc.merge(&mut foreign).unwrap();
let with_foreign = doc.get_heads();
// ... and one of ours.
- doc.put(ROOT, "mine", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "mine", 1_i64).unwrap());
let after = doc.get_heads();
- let missing = missing_from_snapshot(&mut doc, &clock, std::slice::from_ref(&own));
+ let missing = missing_from_snapshot(&doc, &clock, std::slice::from_ref(&own));
assert_eq!(missing.own, 1);
assert_eq!(missing.before, with_foreign);
assert_ne!(missing.before, base);
assert_eq!(
- changed_paths(&mut doc, &missing.before, &after),
+ changed_paths(&doc, &missing.before, &after),
vec![vec!["mine".to_owned()]]
);
// Nothing of ours missing: nothing to replay.
let full: BTreeMap<String, u64> = [(own.to_string(), 2)].into_iter().collect();
- let none = missing_from_snapshot(&mut doc, &full, std::slice::from_ref(&own));
+ let none = missing_from_snapshot(&doc, &full, std::slice::from_ref(&own));
assert_eq!(none.own, 0);
let mut heads = after.clone();
heads.sort();
@@ -525,32 +783,28 @@ mod tests {
#[test]
fn protocol_metadata_is_never_replayed() {
- let mut old = AutoCommit::new();
- old.put(ROOT, "x", 1_i64).unwrap();
- old.commit();
+ let mut old = Document::new();
+ change(&mut old, |old| old.put(ROOT, "x", 1_i64).unwrap());
let base = old.get_heads();
- let mut new = AutoCommit::new();
- store::write_path(
- &mut new,
- &meta::path(&[EPOCHS, "1"]),
- &json!({ "generation": 1 }),
- )
- .unwrap();
- new.commit();
+ let mut new = Document::new();
+ change(&mut new, |new| {
+ store::write_path(
+ new,
+ &meta::path(&[EPOCHS, "1"]),
+ &json!({ "generation": 1 }),
+ )
+ .unwrap()
+ });
// The old epoch replaces `_continuum` wholesale and writes a remake record.
- store::write_path(
- &mut old,
- &meta::path(&[]),
- &json!({ "forgotten": { "pk": 1 } }),
- )
- .unwrap();
- store::write_path(&mut old, &meta::path(&[REMAKES, "c"]), &json!({ "x": 1 })).unwrap();
- old.commit();
+ change(&mut old, |old| {
+ store::write_path(old, &meta::path(&[]), &json!({ "forgotten": { "pk": 1 } })).unwrap();
+ store::write_path(old, &meta::path(&[REMAKES, "c"]), &json!({ "x": 1 })).unwrap();
+ });
let after = old.get_heads();
- replay(&mut old, &base, &after, &mut new, &Translator::default());
+ replay(&old, &base, &after, &mut new, &Translator::default());
let meta = &view(&new)[meta::ROOT];
assert_eq!(meta[EPOCHS]["1"]["generation"], json!(1));
assert_eq!(meta["forgotten"]["pk"], json!(1));