summaryrefslogtreecommitdiffstats
path: root/src/api/store.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-21 04:16:56 +0200
committertv <tv@krebsco.de>2026-10-21 04:16:56 +0200
commitcfde06efcb73f4106620fc7a1d85d06d4c237413 (patch)
treea4a29c8ace80e178e1c26b7b2016b01408ced94c /src/api/store.rs
parentcae74a1abc23aa0e72eca1b5668d9bba87ea4c1e (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.rs32
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);