use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use prikk_error::PrikkError;
use prikk_object::{
BlobKind, BlockKind, BlockPayload, NodeId, NodeKind, ObjectId, ObjectType, text_span_hash,
};
use super::{BlobContentResolver, BlobKindResolver, StoreBackedResolver};
use crate::node_lifecycle::{LiveNode, NodeContent, NodeLifecycleState};
use crate::object_store::ObjectReader;
use crate::patch_replay::decode::{
DecodedDeletePreimage, DecodedOperationKind, decode_patch_operations,
};
use crate::path::RepoPath;
use crate::text_span::{self, TextSpanResolutionFailure};
pub(crate) type TextCache = BTreeMap<NodeId, Vec<u8>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum LifecycleReplayError {
MissingBlockInLineage { block_id: ObjectId },
UnreadableBlockInLineage { block_id: ObjectId, detail: String },
MergeLineageUnsupported {
block_id: ObjectId,
parent_count: usize,
},
LineageCycle { block_id: ObjectId },
HorizonNotInLineage { horizon_id: ObjectId },
InconsistentLifecycleEffect { detail: String },
MalformedPatchInLineage { patch_id: ObjectId, detail: String },
MissingBlobForLifecycleEffect { blob_id: ObjectId },
TextSpanResolutionFailed {
node_id: NodeId,
span_id: [u8; 32],
reason: TextSpanResolutionFailure,
},
}
impl fmt::Display for LifecycleReplayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingBlockInLineage { block_id } => write!(
f,
"lifecycle replay: block {block_id} is missing and cannot be treated as genesis"
),
Self::UnreadableBlockInLineage { block_id, detail } => {
write!(
f,
"lifecycle replay: block {block_id} is unreadable ({detail})"
)
}
Self::MergeLineageUnsupported {
block_id,
parent_count,
} => write!(
f,
"lifecycle replay: block {block_id} has {parent_count} parents and no valid \
mainline parent to derive state from"
),
Self::LineageCycle { block_id } => {
write!(f, "lifecycle replay: cycle detected at block {block_id}")
}
Self::HorizonNotInLineage { horizon_id } => write!(
f,
"lifecycle replay: walk reached genesis without crossing the claimed horizon \
{horizon_id}"
),
Self::InconsistentLifecycleEffect { detail } => write!(
f,
"lifecycle replay: operation could not be applied to the replayed state ({detail})"
),
Self::MalformedPatchInLineage { patch_id, detail } => {
write!(
f,
"lifecycle replay: patch {patch_id} is malformed ({detail})"
)
}
Self::MissingBlobForLifecycleEffect { blob_id } => {
write!(
f,
"lifecycle replay: blob {blob_id} required for a state effect is missing"
)
}
Self::TextSpanResolutionFailed {
node_id,
span_id,
reason,
} => {
write!(f, "lifecycle replay: EditText span on node ")?;
for byte in node_id.as_bytes() {
write!(f, "{byte:02x}")?;
}
write!(f, " (span_id ")?;
for byte in span_id {
write!(f, "{byte:02x}")?;
}
write!(f, ") could not be localized: {reason}")
}
}
}
}
impl From<LifecycleReplayError> for PrikkError {
fn from(value: LifecycleReplayError) -> Self {
Self::Integrity(value.to_string())
}
}
pub(crate) fn read_block(
reader: &impl ObjectReader,
block_id: ObjectId,
) -> Result<BlockPayload, LifecycleReplayError> {
let envelope = reader.read_object(block_id).map_err(|e| {
LifecycleReplayError::UnreadableBlockInLineage {
block_id,
detail: e.to_string(),
}
})?;
let Some(envelope) = envelope else {
return Err(LifecycleReplayError::MissingBlockInLineage { block_id });
};
if envelope.object_type != ObjectType::Block {
return Err(LifecycleReplayError::UnreadableBlockInLineage {
block_id,
detail: format!("object is not a Block ({} found)", envelope.object_type),
});
}
BlockPayload::decode_canonical(&envelope.canonical_payload).map_err(|e| {
LifecycleReplayError::UnreadableBlockInLineage {
block_id,
detail: e.to_string(),
}
})
}
type WalkedBlock = (ObjectId, BlockPayload);
pub(crate) trait LineageBlockReader {
type Block;
fn read_lineage_block(&self, block_id: ObjectId) -> Result<Self::Block, LifecycleReplayError>;
fn parents_of(block: &Self::Block) -> Vec<ObjectId>;
}
struct ReaderLineage<'a, R: ObjectReader>(&'a R);
impl<R: ObjectReader> LineageBlockReader for ReaderLineage<'_, R> {
type Block = BlockPayload;
fn read_lineage_block(&self, block_id: ObjectId) -> Result<BlockPayload, LifecycleReplayError> {
read_block(self.0, block_id)
}
fn parents_of(block: &BlockPayload) -> Vec<ObjectId> {
if block.kind == BlockKind::Merge {
if let Some(mainline) = block.mainline_parent_id {
if block.parent_block_ids.contains(&mainline) {
return vec![mainline];
}
}
}
block.parent_block_ids.clone()
}
}
pub(crate) fn walk_single_parent_chain<R: LineageBlockReader>(
blocks: &R,
baseline: ObjectId,
horizon: ObjectId,
) -> Result<Vec<(ObjectId, R::Block)>, LifecycleReplayError> {
walk_single_parent_chain_inner(blocks, baseline, Some(horizon))
}
fn walk_single_parent_chain_inner<R: LineageBlockReader>(
blocks: &R,
baseline: ObjectId,
expected_horizon: Option<ObjectId>,
) -> Result<Vec<(ObjectId, R::Block)>, LifecycleReplayError> {
let mut chain: Vec<(ObjectId, R::Block)> = Vec::new();
let mut visited: BTreeSet<ObjectId> = BTreeSet::new();
let mut current = baseline;
loop {
if !visited.insert(current) {
return Err(LifecycleReplayError::LineageCycle { block_id: current });
}
let block = blocks.read_lineage_block(current)?;
let next = match R::parents_of(&block).as_slice() {
[] => None,
[parent] => Some(*parent),
other => {
return Err(LifecycleReplayError::MergeLineageUnsupported {
block_id: current,
parent_count: other.len(),
});
}
};
chain.push((current, block));
match next {
None => {
if expected_horizon.is_some_and(|horizon| current != horizon) {
return Err(LifecycleReplayError::HorizonNotInLineage {
horizon_id: expected_horizon.unwrap_or(current),
});
}
break;
}
Some(parent) => current = parent,
}
}
chain.reverse();
Ok(chain)
}
fn walk_lineage(
reader: &impl ObjectReader,
baseline: ObjectId,
horizon: ObjectId,
) -> Result<Vec<WalkedBlock>, LifecycleReplayError> {
walk_single_parent_chain(&ReaderLineage(reader), baseline, horizon)
}
pub(crate) fn replay_lineage(
reader: &impl ObjectReader,
baseline: ObjectId,
horizon: ObjectId,
) -> Result<NodeLifecycleState, LifecycleReplayError> {
let chain = walk_lineage(reader, baseline, horizon)?;
let (state, _text_cache) = replay_chain_with_appended_patches(reader, &chain, &[], false)?;
Ok(state)
}
pub(crate) fn replay_lineage_with_materialized_text(
reader: &impl ObjectReader,
baseline: ObjectId,
horizon: ObjectId,
) -> Result<(NodeLifecycleState, TextCache), LifecycleReplayError> {
let chain = walk_lineage(reader, baseline, horizon)?;
replay_chain_with_appended_patches(reader, &chain, &[], false)
}
pub(crate) fn apply_one_block(
reader: &impl ObjectReader,
block: &BlockPayload,
state: &mut NodeLifecycleState,
require_schema_one: bool,
) -> Result<(), LifecycleReplayError> {
let blob_resolver = if require_schema_one {
StoreBackedResolver::new_format2(reader)
} else {
StoreBackedResolver::new(reader)
};
let mut text_cache = TextCache::new();
apply_patch_ids(
reader,
&block.patch_ids,
&blob_resolver,
state,
&mut text_cache,
require_schema_one,
)
}
pub(crate) fn apply_one_block_with_text_cache(
reader: &impl ObjectReader,
block: &BlockPayload,
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
) -> Result<(), LifecycleReplayError> {
let blob_resolver = StoreBackedResolver::new_format2(reader);
apply_patch_ids(
reader,
&block.patch_ids,
&blob_resolver,
state,
text_cache,
true,
)
}
pub(crate) fn apply_candidate_patches(
reader: &impl ObjectReader,
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
patch_ids: &[ObjectId],
) -> Result<(), LifecycleReplayError> {
let blob_resolver = StoreBackedResolver::new_format2(reader);
apply_patch_ids(reader, patch_ids, &blob_resolver, state, text_cache, true)
}
pub(crate) fn apply_queued_patch_envelopes(
reader: &impl ObjectReader,
records: &[crate::wal::WalRecord],
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
sealed_lineage: Option<(ObjectId, ObjectId)>,
) -> prikk_error::Result<()> {
let blob_resolver = StoreBackedResolver::new(reader);
for record in records {
let patch_id = record.envelope.object_id();
let operations = read_patch_operations_from_envelope(&record.envelope, patch_id)?;
for operation in &operations {
match apply_state_effect(state, text_cache, &operation.kind, &blob_resolver) {
Ok(()) => {}
Err(LifecycleReplayError::MissingBlobForLifecycleEffect { blob_id }) => {
let DecodedOperationKind::EditText { node_id, .. } = &operation.kind else {
return Err(LifecycleReplayError::MissingBlobForLifecycleEffect {
blob_id,
}
.into());
};
let Some((baseline_block_id, horizon_id)) = sealed_lineage else {
return Err(PrikkError::Integrity(format!(
"queued patch {patch_id} edits node {node_id:?} whose content blob \
{blob_id} is missing, and no sealed lineage exists to materialize it \
from"
)));
};
let text = super::materialize_edited_text(
reader,
baseline_block_id,
horizon_id,
*node_id,
)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"queued patch {patch_id} edits node {node_id:?} whose content blob \
{blob_id} is missing and could not be materialized from sealed history"
))
})?;
text_cache.insert(*node_id, text);
apply_state_effect(state, text_cache, &operation.kind, &blob_resolver)?;
}
Err(other) => return Err(other.into()),
}
}
}
Ok(())
}
fn read_patch_operations_from_envelope(
envelope: &prikk_object::ObjectEnvelope,
patch_id: ObjectId,
) -> Result<Vec<crate::patch_replay::decode::DecodedPatchOperation>, LifecycleReplayError> {
if envelope.object_type != ObjectType::Patch {
return Err(LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: format!("object is not a Patch ({} found)", envelope.object_type),
});
}
decode_patch_operations(&envelope.canonical_payload).map_err(|e| {
LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: e.to_string(),
}
})
}
fn replay_chain_with_appended_patches(
reader: &impl ObjectReader,
chain: &[WalkedBlock],
appended_patch_ids: &[ObjectId],
require_schema_one: bool,
) -> Result<(NodeLifecycleState, TextCache), LifecycleReplayError> {
let blob_resolver = if require_schema_one {
StoreBackedResolver::new_format2(reader)
} else {
StoreBackedResolver::new(reader)
};
let mut state = NodeLifecycleState::new();
let mut text_cache = TextCache::new();
for (_block_id, block) in chain {
apply_patch_ids(
reader,
&block.patch_ids,
&blob_resolver,
&mut state,
&mut text_cache,
require_schema_one,
)?;
}
apply_patch_ids(
reader,
appended_patch_ids,
&blob_resolver,
&mut state,
&mut text_cache,
require_schema_one,
)?;
Ok((state, text_cache))
}
fn apply_patch_ids<R: BlobKindResolver + BlobContentResolver>(
reader: &impl ObjectReader,
patch_ids: &[ObjectId],
blob_resolver: &R,
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
require_schema_one: bool,
) -> Result<(), LifecycleReplayError> {
for patch_id in patch_ids {
let operations = read_patch_operations(reader, *patch_id, require_schema_one)?;
for operation in &operations {
apply_state_effect(state, text_cache, &operation.kind, blob_resolver)?;
}
}
Ok(())
}
fn read_patch_operations(
reader: &impl ObjectReader,
patch_id: ObjectId,
require_schema_one: bool,
) -> Result<Vec<crate::patch_replay::decode::DecodedPatchOperation>, LifecycleReplayError> {
let envelope = reader.read_object(patch_id).map_err(|e| {
LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: e.to_string(),
}
})?;
let Some(envelope) = envelope else {
return Err(LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: "patch object is missing".to_string(),
});
};
if envelope.object_type != ObjectType::Patch {
return Err(LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: format!("object is not a Patch ({} found)", envelope.object_type),
});
}
if require_schema_one && envelope.schema_version != 1 {
return Err(LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: format!(
"format-2 Patch requires envelope schema 1, got {}",
envelope.schema_version
),
});
}
decode_patch_operations(&envelope.canonical_payload).map_err(|e| {
LifecycleReplayError::MalformedPatchInLineage {
patch_id,
detail: e.to_string(),
}
})
}
mod effect;
use effect::apply_state_effect;
#[cfg(test)]
mod tests;