Subscriptions

The catch-up-then-live-tail loop — one generic, no-Box machine over any RawEventStore + WakeSource.

Subscriptions

A subscription lets you tail a store: read all existing events, then keep receiving new ones as they are appended, forever. It is how projections stay current and how sagas hear about upstream events. mnesis implements the whole catch-up-then-live-tail loop once, generically, for any adapter that is a RawEventStore + WakeSource.

Subscribing

Build a Subscription from a store handle, then subscribe to one stream or to $all:

rust
let subscription = Subscription::new(&store);

let stream = subscription.subscribe(&id, None)?;   // from the beginning
// or resume strictly after a known version:
let stream = subscription.subscribe(&id, Some(v3))?;

subscribe is synchronous and fallible: it returns Result<impl Stream&lt;..>, WakeError>. Registration failure surfaces eagerly as the Err; read failures stream in-band as Err items — two distinct error domains. The from argument is strict-after: None = beginning, Some(v) = events after v, so reopening never redelivers a duplicate.

The returned stream is !Unpin (it is the unfold of the live loop), so pin! it before consuming — that is the price of the zero-cost, no-Box implementation.

Step — the catch-up → live boundary

Every subscription item is wrapped in a Step, because the boundary between replay and live is what makes a subscription a subscription:

rust
pub enum Step<T> {
    Event(T),    // an event — replay before CaughtUp, live after
    CaughtUp,    // emitted exactly once, at the backlog→live boundary
}

Everything before CaughtUp is history; CaughtUp fires exactly once when you have drained the backlog; everything after is live. The cursor never returns None — when it is caught up it parks (waiting on the WakeSource) rather than ending.

Decoding — phase and decode are orthogonal

subscribe yields raw Step<PersistedEnvelope>. Two combinators (from StepStreamExt / DecodedStreamExt) turn that into what you want — you compose them, they are never welded into one method:

rust
// Typed events, keeping the phase (the projection consumption path):
let mut typed = subscription
    .subscribe(&id, None)?
    .decoded::<AccountEvent, _>(JsonCodec::default());
// yields Step<Decoded<AccountEvent>>

// Drop the phase, get a bare event stream:
let events = subscription.subscribe(&id, None)?.events();
  • .decoded(codec) keeps Step and decodes each Event → tells catch-up from live and hands you typed events (owning codecs).
  • .events() drops the phase → a bare raw stream the full decode surface then applies to.
  • .for_each_decoded(codec, f) — an internal-iteration fold that hands the borrowed window to a closure, so zero-copy codecs (rkyv/bytemuck) work with no lending stream.

Lost-wakeup safety

The loop arms before it confirms it is caught up, then re-scans: it registers its interest in future wakes before checking for a gap, so an event appended in the race window cannot be missed. WakeRegistration::arm captures a "seen version" at arm time, so only a future commit resolves the wait. Spurious wakes are permitted (they cost one empty re-scan); missed wakes are not.

$all subscriptions

subscribe_all(from) tails every stream in append order, yielding Step&lt;(AllPosition, PersistedEnvelope)>. This is the feed a whole-store projection reads; it resumes from a GlobalSeq, which is monotonic but not gapless (an aborted append may burn a value), so tolerate gaps.

Where to next