summaryrefslogtreecommitdiffstats
path: root/src/store.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-20 00:11:18 +0200
committertv <tv@krebsco.de>2026-10-20 00:11:18 +0200
commit172adbac78e97c207cb21dcb832fa377e3ab73ba (patch)
tree794208567f2d5da4bb75c86a88b243bac475b2d6 /src/store.rs
parentf2f714312638d90e8688c40d4a13eb60e8a5b92e (diff)
add compaction
Diffstat (limited to 'src/store.rs')
-rw-r--r--src/store.rs129
1 files changed, 129 insertions, 0 deletions
diff --git a/src/store.rs b/src/store.rs
index cb35272..513728e 100644
--- a/src/store.rs
+++ b/src/store.rs
@@ -301,6 +301,135 @@ pub fn delete_path(doc: &mut AutoCommit, path: &[String], prune: bool) -> Result
Ok(())
}
+// --- materialization ---------------------------------------------------------
+
+/// Copies the merged view of `src` into `dst` as fresh objects.
+///
+/// * Conflicts collapse exactly as reads resolve them: concurrent maps merge, everything
+/// else keeps the newest candidate.
+/// * Automerge types survive: text stays text, counters stay counters (normalized to
+/// their current value), bytes, timestamps and floats are copied bit-exact.
+/// * Map keys are visited in sorted order and list elements in order, so two documents
+/// with the same view produce the same operations in the same order.
+///
+/// `skip` sees the path of every map entry and list element (list indices as decimal
+/// strings); returning true leaves that subtree out.
+#[cfg_attr(not(test), allow(dead_code))] // used by compaction from step 4 on
+pub(crate) fn materialize_into(
+ src: &AutoCommit,
+ dst: &mut AutoCommit,
+ skip: &dyn Fn(&[String]) -> bool,
+) -> Result<(), StoreError> {
+ let mut path = Vec::new();
+ copy_maps(
+ src,
+ &[automerge::ROOT],
+ dst,
+ &automerge::ROOT,
+ &mut path,
+ skip,
+ )
+}
+
+fn copy_maps(
+ src: &AutoCommit,
+ maps: &[ObjId],
+ dst: &mut AutoCommit,
+ target: &ObjId,
+ path: &mut Vec<String>,
+ skip: &dyn Fn(&[String]) -> bool,
+) -> Result<(), StoreError> {
+ // merged_keys is a BTreeSet: sorted, so the operation order is deterministic.
+ for key in merged_keys(src, maps) {
+ path.push(key.clone());
+ if !skip(path) {
+ if let Some(resolved) = resolve(src, maps, &key, None)? {
+ copy_resolved(src, &resolved, dst, target, Slot::Key(&key), path, skip)?;
+ }
+ }
+ path.pop();
+ }
+ Ok(())
+}
+
+fn copy_resolved(
+ src: &AutoCommit,
+ resolved: &Resolved,
+ dst: &mut AutoCommit,
+ parent: &ObjId,
+ slot: Slot,
+ path: &mut Vec<String>,
+ skip: &dyn Fn(&[String]) -> bool,
+) -> Result<(), StoreError> {
+ match resolved {
+ Resolved::Maps(maps) => {
+ let id = put_object(dst, parent, slot, ObjType::Map)?;
+ copy_maps(src, maps, dst, &id, path, skip)
+ }
+ Resolved::Other(Value::Scalar(scalar), _) => {
+ let scalar = match scalar.as_ref() {
+ // A read counter carries its increment history; copy only its value, so
+ // equal counters produce equal operations.
+ ScalarValue::Counter(counter) => ScalarValue::counter(i64::from(counter.clone())),
+ other => other.clone(),
+ };
+ put_scalar(dst, parent, slot, scalar)
+ }
+ Resolved::Other(Value::Object(obj_type), id) => {
+ copy_object(src, *obj_type, id, dst, parent, slot, path, skip)
+ }
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn copy_object(
+ src: &AutoCommit,
+ obj_type: ObjType,
+ id: &ObjId,
+ dst: &mut AutoCommit,
+ parent: &ObjId,
+ slot: Slot,
+ path: &mut Vec<String>,
+ skip: &dyn Fn(&[String]) -> bool,
+) -> Result<(), StoreError> {
+ match obj_type {
+ ObjType::Map => copy_resolved(
+ src,
+ &Resolved::Maps(vec![id.clone()]),
+ dst,
+ parent,
+ slot,
+ path,
+ skip,
+ ),
+ ObjType::List => {
+ let list = put_object(dst, parent, slot, ObjType::List)?;
+ // Skipped elements leave no gap in the copy.
+ let mut position = 0;
+ for (index, item) in src.list_range(id, ..).enumerate() {
+ path.push(index.to_string());
+ if !skip(path) {
+ let child_id = item.id();
+ let child = match Value::from(item.value).into_owned() {
+ Value::Object(ObjType::Map) => Resolved::Maps(vec![child_id]),
+ value => Resolved::Other(value, child_id),
+ };
+ copy_resolved(src, &child, dst, &list, Slot::Index(position), path, skip)?;
+ position += 1;
+ }
+ path.pop();
+ }
+ Ok(())
+ }
+ ObjType::Text => {
+ let text = put_object(dst, parent, slot, ObjType::Text)?;
+ dst.splice_text(&text, 0, 0, &src.text(id)?)?;
+ Ok(())
+ }
+ ObjType::Table => Err(anyhow!("Automerge tables are not supported").into()),
+ }
+}
+
/// Where a value is placed inside its parent object.
#[derive(Clone, Copy)]
enum Slot<'a> {