From 289b84b09a791b88f039664d71bb644c94de6000 Mon Sep 17 00:00:00 2001 From: tv Date: Fri, 30 Oct 2026 19:13:40 +0100 Subject: api: add support for shallow store reads --- src/document/store.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) (limited to 'src/document') diff --git a/src/document/store.rs b/src/document/store.rs index e742edb..89058fb 100644 --- a/src/document/store.rs +++ b/src/document/store.rs @@ -234,6 +234,78 @@ pub(crate) fn read_path_traced( Ok((value, !log.reports.is_empty())) } +/// Strings and bytes up to this long are in a shallow read's summaries. +const SHALLOW_INLINE: usize = 256; + +/// What's at `path` (the empty path is the whole store), one level deep: a summary of +/// it, `type` (`map`, `list`, `text`, `string`, `bytes`, `number`, `boolean`, `null`, +/// or `unknown`, an Automerge scalar of a later version) +/// and `size` (keys, items, characters or bytes), and its `value` where it's a short +/// scalar; a map's has its keys' summaries too, under `entries`. Concurrent values +/// resolved as a read resolves them; conflicts not reported. +pub(crate) fn read_shallow( + doc: &AutoCommit, + path: &[String], +) -> Result, StoreError> { + let Some(resolved) = resolve_at(doc, path)? else { + return Ok(None); + }; + summary(doc, &resolved, true).map(Some) +} + +/// A summary of `resolved` (`read_shallow`); with `entries`, a map's keys' too. +fn summary(doc: &AutoCommit, resolved: &Resolved, entries: bool) -> Result { + let mut out = serde_json::Map::new(); + match resolved { + Resolved::Maps(maps) => { + let keys = merged_keys(doc, maps); + out.insert("type".into(), "map".into()); + out.insert("size".into(), keys.len().into()); + if entries { + let mut listed = serde_json::Map::new(); + for key in keys { + if let Some(child) = resolve(doc, maps, &key, None)? { + listed.insert(key, summary(doc, &child, false)?); + } + } + out.insert("entries".into(), JsonValue::Object(listed)); + } + } + Resolved::Other(Value::Object(ObjType::Map), id) => { + return summary(doc, &Resolved::Maps(vec![id.clone()]), entries); + } + Resolved::Other(Value::Object(ObjType::List), id) => { + out.insert("type".into(), "list".into()); + out.insert("size".into(), doc.length(id).into()); + } + Resolved::Other(Value::Object(ObjType::Text), id) => { + out.insert("type".into(), "text".into()); + out.insert("size".into(), doc.length(id).into()); + } + Resolved::Other(Value::Object(ObjType::Table), _) => { + return Err(anyhow!("Automerge tables are not supported").into()); + } + Resolved::Other(Value::Scalar(scalar), _) => { + let (kind, size) = match &**scalar { + ScalarValue::Str(text) => ("string", Some(text.len())), + ScalarValue::Bytes(bytes) => ("bytes", Some(bytes.len())), + ScalarValue::Null => ("null", None), + ScalarValue::Boolean(_) => ("boolean", None), + ScalarValue::Unknown { .. } => ("unknown", None), + _ => ("number", None), + }; + out.insert("type".into(), kind.into()); + if let Some(size) = size { + out.insert("size".into(), size.into()); + } + if kind != "unknown" && size.is_none_or(|size| size <= SHALLOW_INLINE) { + out.insert("value".into(), scalar_to_json(scalar.clone().into_owned())?); + } + } + } + Ok(JsonValue::Object(out)) +} + /// Keys of the merged map at `path`; empty if it doesn't exist or isn't a map. pub(crate) fn keys_at(doc: &AutoCommit, path: &[String]) -> Result, StoreError> { let mut maps = vec![automerge::ROOT]; -- cgit v1.3.1