///|
/// Creates a checkpoint from the final verified boundary of a replay report.
pub fn[S] checkpoint_from_report(report : ReplayReport[S]) -> Checkpoint[S]? {
match report.status {
Completed =>
Some({
sequence: report.final_sequence,
state: report.final_state,
state_hash: report.final_state_hash,
journal_hash: report.journal_tail_hash,
})
_ => None
}
}
///|
/// Continues a validated journal from an anchored checkpoint.
pub fn[P, S] replay_from_checkpoint(
journal : Journal[P],
checkpoint : Checkpoint[S],
payload_fingerprint : (P) -> String,
state_fingerprint : (S) -> String,
reducer : (S, JournalEvent[P]) -> Transition[S],
) -> ReplayReport[S] {
let actual_state_hash = fingerprint_text(state_fingerprint(checkpoint.state))
let invalid_report = fn(reason : String) -> ReplayReport[S] {
{
status: InvalidCheckpoint(reason),
initial_sequence: checkpoint.sequence,
applied_events: 0,
final_sequence: checkpoint.sequence,
final_state: checkpoint.state,
final_state_hash: actual_state_hash,
journal_tail_hash: journal.tail_hash(),
state_hashes: [actual_state_hash],
}
}
if actual_state_hash != checkpoint.state_hash {
return invalid_report("checkpoint state fingerprint does not match")
}
match journal.validate(payload_fingerprint) {
Invalid(issue) =>
return {
status: InvalidJournal(issue),
initial_sequence: checkpoint.sequence,
applied_events: 0,
final_sequence: checkpoint.sequence,
final_state: checkpoint.state,
final_state_hash: actual_state_hash,
journal_tail_hash: journal.tail_hash(),
state_hashes: [actual_state_hash],
}
Valid(_, _) => ()
}
match journal.hash_at(checkpoint.sequence) {
None => return invalid_report("checkpoint sequence is outside the journal")
Some(anchor) =>
if anchor != checkpoint.journal_hash {
return invalid_report("checkpoint journal anchor does not match")
}
}
let state_hashes : Array[Int] = [actual_state_hash]
let mut state = checkpoint.state
let mut applied = 0
for index = checkpoint.sequence; index < journal.length(); index = index + 1 {
let event = match journal.get(index) {
Some(event) => event
None => abort("validated journal index disappeared")
}
match reducer(state, event) {
Accepted(next_state) => {
state = next_state
applied = applied + 1
state_hashes.push(fingerprint_text(state_fingerprint(state)))
}
Rejected(reason) =>
return {
status: Rejected(event.sequence, reason),
initial_sequence: checkpoint.sequence,
applied_events: applied,
final_sequence: event.sequence - 1,
final_state: state,
final_state_hash: fingerprint_text(state_fingerprint(state)),
journal_tail_hash: journal.tail_hash(),
state_hashes,
}
}
}
{
status: Completed,
initial_sequence: checkpoint.sequence,
applied_events: applied,
final_sequence: checkpoint.sequence + applied,
final_state: state,
final_state_hash: fingerprint_text(state_fingerprint(state)),
journal_tail_hash: journal.tail_hash(),
state_hashes,
}
}