aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/openapi.yaml16
-rw-r--r--src/api/store.rs37
-rw-r--r--src/document/store.rs72
3 files changed, 125 insertions, 0 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index b85481c..ba66f08 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -54,6 +54,7 @@ paths:
parameters:
- $ref: '#/components/parameters/conflicts'
- $ref: '#/components/parameters/heads'
+ - $ref: '#/components/parameters/shallow'
responses:
'200':
$ref: '#/components/responses/Value'
@@ -77,6 +78,7 @@ paths:
parameters:
- $ref: '#/components/parameters/conflicts'
- $ref: '#/components/parameters/heads'
+ - $ref: '#/components/parameters/shallow'
responses:
'200':
$ref: '#/components/responses/Value'
@@ -1001,6 +1003,20 @@ components:
schema:
type: boolean
default: false
+ shallow:
+ name: shallow
+ in: query
+ description: |
+ Instead of the value, what is there one level deep, as JSON, to browse a large
+ store a level at a time: `type` (`map`, `list`, `text`, `string`, `bytes`,
+ `number`, `boolean`, `null`, `unknown`), `size` (a map's keys, a list's items,
+ a text's characters, a string's or bytes' bytes), `value` for a scalar up to
+ 256 bytes, and for a map `entries`: each key's summary (without entries of its
+ own). Concurrent values resolved as a read resolves them. Not with `conflicts`
+ (`400`).
+ schema:
+ type: boolean
+ default: false
wait:
name: wait
in: query
diff --git a/src/api/store.rs b/src/api/store.rs
index d0fc807..7e8caed 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -32,6 +32,10 @@ pub(crate) struct ReadQuery {
/// to base a replacing remake on exactly what was read.
#[serde(default)]
heads: bool,
+ /// What's there, one level deep (`store::read_shallow`), to browse a large store a
+ /// level at a time.
+ #[serde(default)]
+ shallow: bool,
}
#[derive(Debug, Deserialize)]
@@ -54,6 +58,9 @@ pub(crate) async fn get_store_root(
headers: HeaderMap,
Query(query): Query<ReadQuery>,
) -> Result<Response, StatusCode> {
+ if query.shallow {
+ return shallow(&state, Vec::new(), true, &query).await;
+ }
let accept = headers.get(ACCEPT).cloned();
let want_conflicts = query.conflicts;
@@ -91,6 +98,9 @@ pub(crate) async fn get_store_entry(
Query(query): Query<ReadQuery>,
) -> Result<Response, StatusCode> {
let path = parse_key_path(&key)?;
+ if query.shallow {
+ return shallow(&state, path, false, &query).await;
+ }
let accept = headers.get(ACCEPT).cloned();
let want_conflicts = query.conflicts;
@@ -121,6 +131,33 @@ pub(crate) async fn get_store_entry(
mark_source(response, source, heads)
}
+/// `?shallow=true`: what's at `path`, one level deep, as JSON (`store::read_shallow`);
+/// with conflicts asked for too, `400`. `unready_live` as `render_read` has it.
+async fn shallow(
+ state: &AppState,
+ path: Vec<String>,
+ unready_live: bool,
+ query: &ReadQuery,
+) -> Result<Response, StatusCode> {
+ if query.conflicts {
+ return Err(StatusCode::BAD_REQUEST);
+ }
+ let (response, source, heads) = state
+ .render_read(
+ unready_live,
+ query.heads,
+ move |doc| store::read_shallow(doc, &path),
+ move |result| {
+ let value = result
+ .map_err(|e| store_status(e, "Failed to read store path"))?
+ .ok_or(StatusCode::NOT_FOUND)?;
+ response_for_value(&value, None)
+ },
+ )
+ .await?;
+ mark_source(response, source, heads)
+}
+
/// Header on reads served from the pre-rejoin document; its value is that epoch.
const STALE_HEADER: &str = "x-continuum-stale";
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<Option<JsonValue>, 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<JsonValue, StoreError> {
+ 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<Vec<String>, StoreError> {
let mut maps = vec![automerge::ROOT];