Reseed lost event history from the KV projection on transition
Deploy / deploy (push) Successful in 1m0s

A record whose event log is empty (the EVENTS stream was purged, or
the record predates event-sourcing) but whose projection exists used
to be stranded - replay found nothing, every transition failed with
"unknown item". transition_or_reseed reconstructs a minimal one-event
history from the projection's own current state and retries, making
"zap the whole event stream" a recoverable operation by design instead
of a slow-motion data loss. Reseed races are guarded by the same
expected-sequence-0 CAS create uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-12 22:27:57 +02:00
co-authored by Claude Sonnet 5
parent 3233d9a3db
commit 20b24f08c2
2 changed files with 79 additions and 4 deletions
+33
View File
@@ -240,6 +240,39 @@ pub async fn create(
})
}
/// Reconstructs a minimal one-event history for an aggregate whose
/// event log is empty but whose KV projection still knows its current
/// state - what makes wiping the EVENTS stream a recoverable
/// operation rather than one that strands every pre-wipe record in an
/// untransitionable limbo. `expected_seq: Some(0)` guards two racing
/// reseeds the same way `create` guards a double submit.
pub async fn reseed(
js: &async_nats::jetstream::Context,
schema: &AggregateSchema,
id: &str,
state: &str,
occurred_at_ms: i64,
) -> Result<(), TransitionError> {
let event_type = schema.event_for_state.get(state).ok_or_else(|| {
TransitionError::Store(format!(
"cannot reseed {id}: state {state:?} is not declared for bucket {:?}",
schema.bucket
))
})?;
append_event(
js,
&schema.bucket,
id,
event_type,
serde_json::json!({ "reseeded_from_projection": true }),
occurred_at_ms,
Some(0),
)
.await
.map_err(classify_publish_err)?;
Ok(())
}
/// Loads and replays `id`'s current state, then - if `target` is
/// actually reachable from it - appends the corresponding event with
/// the aggregate's just-replayed sequence as the CAS guard.