diff options
| author | tv <tv@krebsco.de> | 2026-10-21 04:16:56 +0200 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-21 04:16:56 +0200 |
| commit | cfde06efcb73f4106620fc7a1d85d06d4c237413 (patch) | |
| tree | a4a29c8ace80e178e1c26b7b2016b01408ced94c /src/api/store.rs | |
| parent | cae74a1abc23aa0e72eca1b5668d9bba87ea4c1e (diff) | |
add heads to reads
GET /store?heads=true and GET /store/{key}?heads=true add the header
x-continuum-heads (sorted, comma-separated hex). The heads come from
the same moment as the read.
- It's opt-in because reading the heads takes the write lock.
Reads without the flag don't change.
- Stale reads during a rejoin never carry heads.
Diffstat (limited to 'src/api/store.rs')
| -rw-r--r-- | src/api/store.rs | 32 |
1 files changed, 28 insertions, 4 deletions
diff --git a/src/api/store.rs b/src/api/store.rs index 8ea0202..409a530 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -2,6 +2,7 @@ use std::io::Error as IoError; +use automerge::ChangeHash; use axum::{ body::{Body, Bytes}, extract::{Path, Query, State}, @@ -25,6 +26,10 @@ use crate::swarm::sync::paths_related; pub(crate) struct ReadQuery { #[serde(default)] conflicts: bool, + /// Return the document's heads at the time of the read (`x-continuum-heads`), e.g. + /// to base a replacing remake on exactly what was read. + #[serde(default)] + heads: bool, } #[derive(Debug, Deserialize)] @@ -47,6 +52,7 @@ fn conflicts_response( tag = "Store", params( ("conflicts" = Option<bool>, Query, description = "Wrap the value and list every conflict resolved on the way"), + ("heads" = Option<bool>, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), ), responses( (status = 200, description = "Returns the complete store as JSON or text, depending on Accept"), @@ -62,9 +68,10 @@ pub(crate) async fn get_store_root( let accept = headers.get(ACCEPT).cloned(); let want_conflicts = query.conflicts; - let (response, source) = state + let (response, source, heads) = state .render_read( true, + query.heads, move |doc| { if want_conflicts { store::read_path_with_conflicts(doc, &Vec::new()) @@ -84,7 +91,7 @@ pub(crate) async fn get_store_root( }, ) .await?; - mark_source(response, source) + mark_source(response, source, heads) } #[utoipa::path( @@ -94,6 +101,7 @@ pub(crate) async fn get_store_root( params( ("key" = String, Path, description = "Slash-separated store path, for example settings/theme"), ("conflicts" = Option<bool>, Query, description = "Wrap the value and list every conflict resolved on the way"), + ("heads" = Option<bool>, Query, description = "Return the document heads at the time of the read in x-continuum-heads"), ), responses( (status = 200, description = "Returns the value as JSON or text, depending on Accept"), @@ -114,9 +122,10 @@ pub(crate) async fn get_store_entry( let accept = headers.get(ACCEPT).cloned(); let want_conflicts = query.conflicts; - let (response, source) = state + let (response, source, heads) = state .render_read( false, + query.heads, move |doc| { if want_conflicts { store::read_path_with_conflicts(doc, &path) @@ -136,17 +145,32 @@ pub(crate) async fn get_store_entry( }, ) .await?; - mark_source(response, source) + 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"; +/// Header with the document's heads at the time of the read (sorted, comma-separated +/// hex), on request (`?heads=true`). +const HEADS_HEADER: &str = "x-continuum-heads"; + fn mark_source( response: Result<Response, StatusCode>, source: ReadSource, + heads: Option<Vec<ChangeHash>>, ) -> Result<Response, StatusCode> { let mut response = response?; + if let Some(heads) = heads { + let joined = heads + .iter() + .map(ToString::to_string) + .collect::<Vec<_>>() + .join(","); + if let Ok(value) = HeaderValue::from_str(&joined) { + response.headers_mut().insert(HEADS_HEADER, value); + } + } if let ReadSource::Stale(epoch) = source { if let Ok(value) = HeaderValue::from_str(&epoch.to_string()) { response.headers_mut().insert(STALE_HEADER, value); |
