///|
/// In-memory append-only journal.
///
/// Persistence adapters can reconstruct a journal with `from_events` and then
/// call `validate` before allowing replay.
pub struct Journal[P] {
records : Array[JournalEvent[P]]
}
///|
/// Creates an empty event journal.
pub fn[P] Journal::new() -> Journal[P] {
{ records: [] }
}
///|
/// Reconstructs a journal from externally loaded events.
///
/// The caller must validate the returned journal before replay.
pub fn[P] Journal::from_events(events : Array[JournalEvent[P]]) -> Journal[P] {
{ records: events.copy() }
}
///|
/// Returns the number of events in the journal.
pub fn[P] Journal::length(self : Journal[P]) -> Int {
self.records.length()
}
///|
/// Returns an event by zero-based index.
pub fn[P] Journal::get(self : Journal[P], index : Int) -> JournalEvent[P]? {
self.records.get(index)
}
///|
/// Returns a copy suitable for persistence or diagnostics.
pub fn[P] Journal::events(self : Journal[P]) -> Array[JournalEvent[P]] {
self.records.copy()
}
///|
/// Returns a journal containing the first `count` events.
///
/// The returned journal preserves the original hash evidence and should still
/// be validated before replay.
pub fn[P] Journal::prefix(self : Journal[P], count : Int) -> Journal[P] {
let events : Array[JournalEvent[P]] = []
let limit = if count < 0 {
0
} else if count > self.records.length() {
self.records.length()
} else {
count
}
for index = 0; index < limit; index = index + 1 {
events.push(self.records[index])
}
Journal::from_events(events)
}
///|
/// Returns the hash-chain anchor at a sequence boundary.
pub fn[P] Journal::hash_at(self : Journal[P], sequence : Int) -> Int? {
if sequence == 0 {
Some(0)
} else if sequence < 0 || sequence > self.records.length() {
None
} else {
Some(self.records[sequence - 1].hash)
}
}
///|
/// Returns the current hash-chain tail, or zero for an empty journal.
pub fn[P] Journal::tail_hash(self : Journal[P]) -> Int {
if self.records.length() == 0 {
0
} else {
self.records[self.records.length() - 1].hash
}
}
///|
/// Appends one event and returns its complete journal record.
pub fn[P] Journal::append(
self : Journal[P],
kind : String,
payload : P,
correlation_id : String,
payload_fingerprint : (P) -> String,
) -> JournalEvent[P] {
let sequence = self.records.length() + 1
let previous_hash = self.tail_hash()
let hash = event_fingerprint(
previous_hash,
sequence,
kind,
correlation_id,
payload_fingerprint(payload),
)
let event = { sequence, kind, payload, correlation_id, previous_hash, hash }
self.records.push(event)
event
}
///|
/// Validates sequence continuity and the complete event hash chain.
pub fn[P] Journal::validate(
self : Journal[P],
payload_fingerprint : (P) -> String,
) -> ChainValidation {
let mut previous_hash = 0
for index = 0; index < self.records.length(); index = index + 1 {
let event = self.records[index]
let expected_sequence = index + 1
if event.sequence != expected_sequence {
return Invalid(
UnexpectedSequence(index, expected_sequence, event.sequence),
)
}
if event.previous_hash != previous_hash {
return Invalid(
BrokenPreviousHash(event.sequence, previous_hash, event.previous_hash),
)
}
let expected_hash = event_fingerprint(
previous_hash,
event.sequence,
event.kind,
event.correlation_id,
payload_fingerprint(event.payload),
)
if event.hash != expected_hash {
return Invalid(
InvalidEventHash(event.sequence, expected_hash, event.hash),
)
}
previous_hash = event.hash
}
Valid(self.records.length(), previous_hash)
}