summaryrefslogtreecommitdiffstats
path: root/src/remake/kind.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/kind.rs')
-rw-r--r--src/remake/kind.rs52
1 files changed, 28 insertions, 24 deletions
diff --git a/src/remake/kind.rs b/src/remake/kind.rs
index 685fa28..8131f7f 100644
--- a/src/remake/kind.rs
+++ b/src/remake/kind.rs
@@ -28,11 +28,12 @@ use crate::document::store::{self, StoreError};
/// Name of the kind used when a request doesn't name one.
pub(crate) const DEFAULT_KIND: &str = "compact";
-/// What the coordinator stores in the proposal for every node.
+/// What the coordinator prepares for every node.
#[derive(Debug, Default)]
pub(crate) struct Prepared {
- /// Input to [`RemakeKind::build`]; replicated with the proposal (old epoch only).
- pub(crate) content: JsonValue,
+ /// Input to [`RemakeKind::build`], if the kind takes any. The proposal refers to it
+ /// by hash; voters fetch it from the coordinator and each other (`content`).
+ pub(crate) content: Option<Vec<u8>>,
/// Input to [`RemakeKind::translate`]; also kept in the new epoch's metadata, so
/// nodes that rejoin later can translate their writes.
pub(crate) translation: JsonValue,
@@ -64,7 +65,7 @@ pub(crate) trait RemakeKind: Send + Sync + 'static {
fn build(
&self,
old_at_base: &AutoCommit,
- content: &JsonValue,
+ content: Option<&[u8]>,
target: &mut AutoCommit,
) -> Result<(), StoreError>;
@@ -107,7 +108,7 @@ impl RemakeKind for Compact {
fn build(
&self,
old_at_base: &AutoCommit,
- _content: &JsonValue,
+ _content: Option<&[u8]>,
target: &mut AutoCommit,
) -> Result<(), StoreError> {
store::materialize_into(old_at_base, target, &is_reserved)
@@ -199,8 +200,10 @@ impl RemakeKind for Replace {
));
}
+ let content =
+ serde_json::to_vec(&canonical(spec.document)).map_err(|error| error.to_string())?;
Ok(Prepared {
- content: canonical(spec.document),
+ content: Some(content),
translation: prepare_rules(spec.rules)?,
})
}
@@ -208,12 +211,15 @@ impl RemakeKind for Replace {
fn build(
&self,
_old_at_base: &AutoCommit,
- content: &JsonValue,
+ content: Option<&[u8]>,
target: &mut AutoCommit,
) -> Result<(), StoreError> {
- // Canonical again: the content went through the old epoch's document, and the
- // operations (hence the epoch ID) must not depend on any key order.
- let JsonValue::Object(document) = canonical(content.clone()) else {
+ let content = content.ok_or_else(|| anyhow::anyhow!("replace without content"))?;
+ let document: JsonValue = serde_json::from_slice(content)
+ .map_err(|error| anyhow::anyhow!("replacement document: {error}"))?;
+ // Canonical again: the operations (hence the epoch ID) must not depend on any
+ // key order.
+ let JsonValue::Object(document) = canonical(document) else {
return Ok(()); // propose only accepts objects
};
for (key, value) in document {
@@ -300,11 +306,8 @@ struct ImportSpec {
rules: Vec<RuleSpec>,
}
-/// The document in `content` (base64 of a saved Automerge document).
-fn load_content(content: &JsonValue) -> Result<AutoCommit, String> {
- let JsonValue::String(encoded) = content else {
- return Err("the document must be a base64 string".to_owned());
- };
+/// The document in a request (base64 of a saved Automerge document).
+fn load_encoded(encoded: &str) -> Result<AutoCommit, String> {
let bytes = BASE64
.decode(encoded.trim())
.map_err(|error| format!("the document is not valid base64: {error}"))?;
@@ -320,17 +323,17 @@ impl RemakeKind for Import {
fn propose(&self, spec: &JsonValue) -> Result<Prepared, String> {
let spec: ImportSpec =
serde_json::from_value(spec.clone()).map_err(|error| error.to_string())?;
- let imported = load_content(&JsonValue::String(spec.document))?;
+ let imported = load_encoded(&spec.document)?;
// Keep only what goes into the new epoch: the merged view without protocol data
- // and history. Usually much smaller than what was uploaded, and it is what gets
- // replicated with the proposal.
+ // and history. Usually much smaller than what was uploaded, and it is what every
+ // voter downloads.
let mut content = AutoCommit::new();
store::materialize_into(&imported, &mut content, &is_reserved)
.map_err(|error| format!("the document can't be read: {error}"))?;
Ok(Prepared {
- content: JsonValue::String(BASE64.encode(content.save())),
+ content: Some(content.save()),
translation: prepare_rules(spec.rules)?,
})
}
@@ -338,10 +341,11 @@ impl RemakeKind for Import {
fn build(
&self,
_old_at_base: &AutoCommit,
- content: &JsonValue,
+ content: Option<&[u8]>,
target: &mut AutoCommit,
) -> Result<(), StoreError> {
- let imported = load_content(content).map_err(|reason| anyhow::anyhow!(reason))?;
+ let content = content.ok_or_else(|| anyhow::anyhow!("import without content"))?;
+ let imported = AutoCommit::load(content)?;
// Deterministic: the same bytes give the same view, copied in a fixed order.
store::materialize_into(&imported, target, &is_reserved)
}
@@ -412,7 +416,7 @@ mod tests {
fn build(
&self,
_old: &AutoCommit,
- _content: &JsonValue,
+ _content: Option<&[u8]>,
_target: &mut AutoCommit,
) -> Result<(), StoreError> {
Ok(())
@@ -559,7 +563,7 @@ mod tests {
let build = || {
let mut target = AutoCommit::new().with_actor(automerge::ActorId::from(&b"fixed"[..]));
Import
- .build(&AutoCommit::new(), &prepared.content, &mut target)
+ .build(&AutoCommit::new(), prepared.content.as_deref(), &mut target)
.unwrap();
target.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
target
@@ -591,7 +595,7 @@ mod tests {
let prepared = replace(json!({ "document": document })).unwrap();
let mut target = AutoCommit::new().with_actor(automerge::ActorId::from(&b"fixed"[..]));
Replace
- .build(&AutoCommit::new(), &prepared.content, &mut target)
+ .build(&AutoCommit::new(), prepared.content.as_deref(), &mut target)
.unwrap();
target.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
(