Skip to main content

mnesis_store/
store.rs

1use alloc::sync::Arc;
2
3use mnesis::{ErrorId, Version};
4
5use crate::envelope::{PendingBatch, PersistedEnvelope};
6use crate::error::{AppendError, AppendValidationError};
7use crate::stream::EventStream;
8use crate::stream_id::StreamKey;
9
10// ═══════════════════════════════════════════════════════════════════════════
11// Store<S> — Arc-wrapped handle to a RawEventStore backend
12// ═══════════════════════════════════════════════════════════════════════════
13
14/// Shared handle to a [`RawEventStore`] backend.
15///
16/// `Store` wraps the backend in an `Arc`, making it cheap to clone and
17/// safe to share across tasks. It carries no codec, upcaster, or
18/// aggregate binding — it is just a database handle.
19///
20/// Use [`repository()`](Store::repository) to obtain a
21/// [`RepositoryBuilder`](crate::builder::RepositoryBuilder), then
22/// configure a codec and upcaster before calling `.build()`.
23///
24/// # Example
25///
26/// ```ignore
27/// // Open flows left-to-right; `.into_store()` is the de-nested `Store::new`.
28/// let store = FjallStore::builder("path").open()?.into_store();
29///
30/// // One per-aggregate facade per aggregate; the store is the shared substrate.
31/// let orders = store.repository::<Order>().codec(OrderCodec).build();
32/// let users  = store.repository::<User>().codec(UserCodec).build();
33/// ```
34#[derive(Debug)]
35pub struct Store<S> {
36    inner: Arc<S>,
37}
38
39impl<S> Store<S> {
40    /// Wrap a raw event store backend in a shared handle.
41    pub fn new(raw: S) -> Self {
42        Self {
43            inner: Arc::new(raw),
44        }
45    }
46
47    /// Borrow the underlying raw store.
48    ///
49    /// The escape hatch for users who need the substrate directly — when
50    /// the [`Repository`](crate::Repository) facade's `load` / `save` isn't
51    /// flexible enough (e.g. you want to filter, peek, branch, or chain
52    /// custom combinators during load). Hand the borrowed `&S` to
53    /// [`RawEventStore::read_stream`] / [`RawEventStore::append`] and
54    /// compose your own chain via [`futures::StreamExt`] /
55    /// [`futures::TryStreamExt`].
56    ///
57    /// Users who just want "load this aggregate" should stay on the facade.
58    ///
59    /// # Example
60    ///
61    /// Substrate-path read: convert the adapter error eagerly and drive
62    /// a custom fold.
63    ///
64    /// ```ignore
65    /// use futures::TryStreamExt;
66    /// use mnesis_store::{RawEventStore, Store, StreamKey};
67    ///
68    /// async fn count_events<S: RawEventStore>(
69    ///     store: &Store<S>,
70    ///     id: &StreamKey,
71    ///     from: mnesis::Version,
72    /// ) -> Result<usize, MyError> {
73    ///     let stream = store.raw().read_stream(id, from).await.map_err(MyError::Adapter)?;
74    ///     stream.map_err(MyError::Adapter).try_fold(0usize, |acc, _| async move { Ok(acc + 1) }).await
75    /// }
76    /// ```
77    ///
78    /// [`RawEventStore`]: crate::RawEventStore
79    /// [`RawEventStore::read_stream`]: crate::RawEventStore::read_stream
80    /// [`RawEventStore::append`]: crate::RawEventStore::append
81    #[must_use]
82    pub fn raw(&self) -> &S {
83        &self.inner
84    }
85
86    /// Borrow the inner `Arc<S>` for the subscription module's use.
87    ///
88    /// `pub(crate)` so the subscription module can pull the `Arc` out for
89    /// [`Subscription::new`](crate::subscription::Subscription::new) without
90    /// leaking `Arc` to library users. Only the `subscription` feature needs
91    /// it, so it is gated to avoid a dead-code warning otherwise.
92    #[cfg(feature = "subscription")]
93    #[must_use]
94    pub(crate) const fn arc(&self) -> &Arc<S> {
95        &self.inner
96    }
97}
98
99impl<S> Clone for Store<S> {
100    fn clone(&self) -> Self {
101        Self {
102            inner: Arc::clone(&self.inner),
103        }
104    }
105}
106
107// ═══════════════════════════════════════════════════════════════════════════
108// RawEventStore<M> — byte-level append + read_stream trait
109// ═══════════════════════════════════════════════════════════════════════════
110
111/// What database adapters implement. Bytes in, bytes out.
112///
113/// Knows nothing about typed events or codecs. The `EventStore` facade
114/// calls this trait after encoding events into `PendingEnvelope`.
115pub trait RawEventStore: Send + Sync {
116    /// The error type for store operations.
117    type Error: core::error::Error + Send + Sync + 'static;
118
119    /// The stream type for reading events.
120    ///
121    /// Owned, non-GAT, `'static` — a `futures::Stream` of
122    /// `Result<PersistedEnvelope, Self::Error>`. The owned-`Bytes`
123    /// envelope means cursors don't need to lend per-record; the
124    /// stream's `Item` is the envelope by value.
125    ///
126    /// Note: the subscription path ([`Subscription::subscribe`]) requires the
127    /// stream be `Unpin`. No bound is imposed here, but all shipped adapters
128    /// (`ScanCursor`, `InMemoryStream`) satisfy it.
129    ///
130    /// [`Subscription::subscribe`]: crate::subscription::Subscription::subscribe
131    type Stream: EventStream<Error = Self::Error> + 'static;
132
133    /// The adapter-defined `$all` resume position. See [`AllPosition`].
134    ///
135    /// A scalar for an embedded store (fjall's `GlobalSeq`), a commit-ordered
136    /// composite for a concurrent SQL store (postgres's `(txid, seq)`). It rides
137    /// *alongside* `$all` events on [`AllStream`](Self::AllStream); the
138    /// (position-free) [`PersistedEnvelope`] never carries it.
139    type AllPosition: AllPosition;
140
141    /// The stream type for an all-streams (`$all`) read.
142    ///
143    /// Owned, non-GAT, `'static` — a `futures::Stream` of
144    /// `Result<(Self::AllPosition, StreamKey, PersistedEnvelope), Self::Error>`,
145    /// ascending by [`AllPosition`], not by `(stream, version)`. Each item
146    /// carries three parts: the **position** for checkpointing (resume needs no
147    /// global field on the envelope), the **stream key for routing** (the store
148    /// knows the origin stream at append time, so an `$all` consumer routes on
149    /// raw id bytes without decoding the payload), and the **envelope** for
150    /// content. Distinct from [`Stream`](Self::Stream) because the global order
151    /// is a different physical index.
152    ///
153    /// Note: the subscription path ([`Subscription::subscribe`]) requires the
154    /// stream be `Unpin`. No bound is imposed here, but all shipped adapters
155    /// (`ScanCursor`, `InMemoryStream`) satisfy it.
156    ///
157    /// [`Subscription::subscribe`]: crate::subscription::Subscription::subscribe
158    type AllStream: futures::Stream<
159            Item = Result<(Self::AllPosition, StreamKey, PersistedEnvelope), Self::Error>,
160        > + Send
161        + 'static;
162
163    /// Append events to a stream with optimistic concurrency.
164    ///
165    /// `expected_version` is the version the aggregate was at before
166    /// new events were applied. The adapter checks this against the
167    /// current stream version and rejects if they don't match.
168    ///
169    /// # Atomicity
170    ///
171    /// The version check and event insertion **must** be atomic. If they
172    /// are separate operations (e.g. SELECT then INSERT), a concurrent
173    /// writer can slip in between, corrupting the stream. Use
174    /// transactions, CAS operations, or a lock to prevent this.
175    ///
176    /// # Implementor contract
177    ///
178    /// Envelopes **must** have strictly sequential versions starting from
179    /// `expected_version + 1`. Implementations **must** reject batches
180    /// where versions are out of order, have gaps, or contain duplicates.
181    /// Accepting malformed batches corrupts the event stream.
182    ///
183    /// # `$all` position
184    ///
185    /// Each appended event is assigned an adapter-defined
186    /// [`AllPosition`](Self::AllPosition) — the order an `$all` subscription
187    /// resumes from. It is **not** carried on the [`PersistedEnvelope`]; on the
188    /// read path it is surfaced as a tag on each
189    /// [`AllStream`](Self::AllStream) item. The position **must** be
190    /// monotonically increasing across *all* streams in commit order but is
191    /// **not** required to be gapless — an adapter may skip values (e.g. after
192    /// an aborted append), and readers must tolerate gaps.
193    ///
194    /// # Return value
195    ///
196    /// On success this returns the position assigned to the run's **last**
197    /// event — the same position `$all` reports for that event. It is the
198    /// read-your-writes token (#330): a caller awaits its `$all` consumer's
199    /// checkpoint reaching this value and then knows its own write is visible
200    /// there. The last event's position is the whole run's barrier because
201    /// `$all` delivery is position-ordered, so a consumer that has reached it
202    /// has necessarily been delivered every earlier event of the same append.
203    ///
204    /// Implementors **must** return the position actually assigned, not a
205    /// counter read back afterwards — the value has to be the one the
206    /// committing transaction stamped, or a concurrent append can make the two
207    /// disagree.
208    ///
209    /// # Non-empty input
210    ///
211    /// [`PendingBatch`] carries at least one envelope, so there is no
212    /// write-nothing case and the returned position is unconditional. An
213    /// adapter therefore never has to answer "what position did zero events
214    /// land at". Note this removes the former version-assertion probe
215    /// (`append(id, expected, &[])`, which checked `expected_version` and wrote
216    /// nothing); it had no production caller and returns additively as its own
217    /// method if it is ever wanted.
218    fn append(
219        &self,
220        id: &StreamKey,
221        expected_version: Option<Version>,
222        envelopes: PendingBatch<'_>,
223    ) -> impl core::future::Future<Output = Result<Self::AllPosition, AppendError<Self::Error>>> + Send;
224
225    /// Open a stream of events.
226    ///
227    /// Events are yielded one at a time as a `futures::Stream` of
228    /// owned [`PersistedEnvelope`](crate::envelope::PersistedEnvelope)s.
229    ///
230    /// `from` is **inclusive**: the stream yields every event with
231    /// `version >= from`, in ascending `Version` order, then terminates with
232    /// `None`. This matches [`read_all`](Self::read_all)'s `from` semantics;
233    /// the catchup seam relies on this inclusivity to resume without skipping
234    /// the boundary event.
235    ///
236    /// # Batching
237    ///
238    /// An adapter **may** chunk or paginate internally (e.g. materialize a
239    /// fixed number of rows at a time and keyset-resume on the stream version
240    /// as the cursor drains) but is **not** required to — bounding resident
241    /// memory is the adapter's concern. Whatever it does is invisible to
242    /// callers: `next()` yields events in ascending `Version` order from `from`
243    /// (inclusive) and returns `None` once the persisted stream is exhausted,
244    /// regardless of how the events are chunked. Memory is bounded by the
245    /// adapter's implementation — fjall, for instance, uses a single lazy LSM
246    /// cursor rather than fixed-size batches.
247    fn read_stream(
248        &self,
249        id: &StreamKey,
250        from: Version,
251    ) -> impl core::future::Future<Output = Result<Self::Stream, Self::Error>> + Send;
252
253    /// Open a one-shot read over **all** streams, ordered by
254    /// [`AllPosition`](Self::AllPosition).
255    ///
256    /// `from` is **exclusive**: the stream yields every event *strictly after*
257    /// `from` (`None` = from the very beginning), in ascending
258    /// [`AllPosition`](Self::AllPosition) order, each item **tagged** with its
259    /// position, then terminates with `None`. Resume is `Ord`-based with no
260    /// successor function — the live loop reopens with the last-delivered
261    /// position and the adapter reads "strictly greater". The position sequence
262    /// is monotonic but **not** gapless; this read tolerates gaps by scanning a
263    /// range rather than stepping a successor.
264    ///
265    /// The exclusive `from` here is an **intentional** asymmetry with
266    /// [`read_stream`](Self::read_stream)'s **inclusive** `Version` `from`
267    /// (CLAUDE rule 4): a single stream has a gapless successor sequence, but a
268    /// concurrent adapter's composite `$all` position has none.
269    ///
270    /// This is the building block under an all-streams subscription; the
271    /// never-ending wait-when-caught-up behaviour is layered on top.
272    ///
273    /// # Stream attribution
274    ///
275    /// Each item carries the [`StreamKey`] of the stream the event was appended
276    /// to — a **store guarantee**, not a payload convention. The per-stream
277    /// read ([`read_stream`](Self::read_stream)) deliberately does NOT stamp
278    /// it: there the id is the query argument and every returned envelope
279    /// belongs to it by construction (intentional read-path asymmetry).
280    ///
281    /// # Batching
282    ///
283    /// Like [`read_stream`](Self::read_stream), an adapter **may** chunk or
284    /// paginate internally (keyset-resume on the position) but is **not**
285    /// required to. The externally-observable contract is unchanged: events are
286    /// yielded in ascending position order strictly after `from`, the stream
287    /// terminates with `None` when caught up, and resident memory is bounded by
288    /// the adapter's implementation.
289    fn read_all(
290        &self,
291        from: Option<Self::AllPosition>,
292    ) -> impl core::future::Future<Output = Result<Self::AllStream, Self::Error>> + Send;
293
294    /// Wrap this backend in a shared [`Store`] handle.
295    ///
296    /// The de-nested alternative to [`Store::new(self)`](Store::new): opening a
297    /// store reads left-to-right —
298    /// `FjallStore::builder(path).open()?.into_store()` — instead of the
299    /// inside-out `Store::new(FjallStore::builder(path).open()?)`. Exactly
300    /// equivalent to `Store::new`; every adapter gets it for free as a provided
301    /// method, and the raw backend stays reachable via [`Store::raw`].
302    #[must_use]
303    fn into_store(self) -> Store<Self>
304    where
305        Self: Sized,
306    {
307        Store::new(self)
308    }
309}
310
311// ═══════════════════════════════════════════════════════════════════════════
312// Store<S> as a delegating RawEventStore — the front door (issue #247)
313// ═══════════════════════════════════════════════════════════════════════════
314
315/// `Store<S>` is itself a [`RawEventStore`], forwarding every method to its
316/// inner backend.
317///
318/// This makes the handle the front door: `store.append(..)` / `read_stream` /
319/// `read_all` work directly, and — because [`EventExporter`] and
320/// [`EventImporter`] are blanket-impl'd for every `RawEventStore` (and
321/// `RawEventStore + AtomicAppend`) — `store.export_stream(..)` /
322/// `store.import(..)` come for free once `Store<S>` also forwards
323/// [`StreamLister`] / [`AtomicAppend`] (in the `export` / `import` modules). So
324/// a `Store<S>` holder never needs `.raw()` to back up or restore, and a
325/// `Store<S>` is substitutable wherever a `RawEventStore`-bounded value is
326/// expected. `.raw()` remains the escape hatch for reaching the concrete `&S`.
327///
328/// [`EventExporter`]: crate::export::EventExporter
329/// [`EventImporter`]: crate::import::EventImporter
330/// [`StreamLister`]: crate::export::StreamLister
331/// [`AtomicAppend`]: crate::import::AtomicAppend
332impl<S: RawEventStore> RawEventStore for Store<S> {
333    type Error = S::Error;
334    type Stream = S::Stream;
335    type AllPosition = S::AllPosition;
336    type AllStream = S::AllStream;
337
338    async fn append(
339        &self,
340        id: &StreamKey,
341        expected_version: Option<Version>,
342        envelopes: PendingBatch<'_>,
343    ) -> Result<Self::AllPosition, AppendError<Self::Error>> {
344        self.raw().append(id, expected_version, envelopes).await
345    }
346
347    async fn read_stream(
348        &self,
349        id: &StreamKey,
350        from: Version,
351    ) -> Result<Self::Stream, Self::Error> {
352        self.raw().read_stream(id, from).await
353    }
354
355    async fn read_all(
356        &self,
357        from: Option<Self::AllPosition>,
358    ) -> Result<Self::AllStream, Self::Error> {
359        self.raw().read_all(from).await
360    }
361}
362
363// ═══════════════════════════════════════════════════════════════════════════
364// AllPosition — adapter-defined `$all` resume position
365// ═══════════════════════════════════════════════════════════════════════════
366
367/// Where an `$all` subscription resumes — **adapter-defined**.
368///
369/// A scalar for an embedded store (fjall's `GlobalSeq`), a commit-ordered
370/// composite for a concurrent SQL store (postgres's `(txid, seq)`), an LSN for
371/// a WAL tail. `mnesis-store` owns only this trait — the *abstraction*; the
372/// concrete position lives in the adapter (dependency direction: the store
373/// cannot reference its adapters), and it is **never** carried on the
374/// position-free [`PersistedEnvelope`].
375///
376/// # Carried alongside events, not derived from them
377///
378/// The position rides on each [`AllStream`](RawEventStore::AllStream) item as a
379/// tag `(AllPosition, StreamKey, PersistedEnvelope)`. A consumer checkpoints
380/// the position it last saw and hands it back to
381/// [`read_all`](RawEventStore::read_all) /
382/// `subscribe_all` to resume — so the consumer's checkpoint type is
383/// adapter-defined and must be serializable (fjall: a `u64`; postgres: a pair).
384///
385/// # `Ord`, no successor
386///
387/// The live loop resumes **strictly after** the last delivered position using
388/// [`Ord`] alone. There is deliberately no `next`/successor: a composite
389/// position such as `(txid, seq)` has no natural `+1` in `txid` space, and the
390/// `$all` read is **exclusive** (`WHERE pos > from`), so `Ord` is all the loop
391/// needs.
392///
393/// # Not a distributed clock
394///
395/// An `AllPosition` orders one store's appends; it is **not** a cross-producer
396/// or causal timestamp. A distributed adapter does not widen or reinterpret it:
397/// causal/HLC metadata rides in the event's `metadata` bytes, the store never
398/// orders by it, and merging across producers is the consumer's job.
399pub trait AllPosition: Copy + Ord + Send + Sync + core::fmt::Debug + 'static {}
400
401/// Validate the append contract once, for every adapter.
402///
403/// `current` is the stream's current max version (`0` = a fresh stream that has
404/// never been appended to). This enforces the two invariants the
405/// [`RawEventStore::append`] doc specifies in prose but leaves unimplemented:
406///
407/// 1. **Optimistic concurrency** — `expected` must equal `current` (a fresh
408///    stream requires `expected == None`; a non-empty stream requires
409///    `expected == Some(current)`). Any mismatch is an
410///    [`AppendValidationError::Conflict`].
411/// 2. **Strict-sequentiality** — `envelopes` must be `current+1, current+2, …`,
412///    overflow-checked. A gap, out-of-order entry, or `u64::MAX` overflow is a
413///    `Conflict` (gap/out-of-order) or [`AppendValidationError::VersionOverflow`]
414///    (overflow — never a retry-eligible `Conflict`, rule 3).
415///
416/// Adapters call this *inside* their transaction/lock, **before** any staging or
417/// wire encoding, then map the neutral [`AppendValidationError`] into their own
418/// `AppendError`. Centralising it here means the contract has one source of truth
419/// instead of being copy-pasted (and silently drifted) across every adapter.
420///
421/// # Errors
422///
423/// - [`AppendValidationError::Conflict`] — `expected` does not match `current`
424///   (optimistic-concurrency mismatch), or an envelope's version is not the
425///   strict successor of its predecessor (a gap or out-of-order entry).
426/// - [`AppendValidationError::VersionOverflow`] — the version sequence would
427///   advance past `u64::MAX` (never reported as a retry-eligible `Conflict`).
428pub fn validate_append_versions(
429    current: u64,
430    expected: Option<Version>,
431    envelopes: PendingBatch<'_>,
432    id: &StreamKey,
433) -> Result<(), AppendValidationError> {
434    // 1. Optimistic concurrency. `actual` is `None` for a fresh stream so that
435    //    `expected == None` is the only valid expectation there.
436    let actual: Option<Version> = if current == 0 {
437        None
438    } else {
439        Version::new(current)
440    };
441    if expected != actual {
442        return Err(AppendValidationError::Conflict {
443            stream_id: ErrorId::from_display(id),
444            expected,
445            actual,
446        });
447    }
448
449    // 2. Strict-sequentiality. A running `checked_add` counter — no index→u64
450    //    cast, overflow-safe near `u64::MAX` (rule 2).
451    let mut next = current;
452    for env in envelopes {
453        next = next
454            .checked_add(1)
455            .ok_or(AppendValidationError::VersionOverflow)?;
456        if env.version().as_u64() != next {
457            return Err(AppendValidationError::Conflict {
458                stream_id: ErrorId::from_display(id),
459                expected: Version::new(next),
460                actual: Some(env.version()),
461            });
462        }
463    }
464    Ok(())
465}
466
467#[cfg(test)]
468#[allow(clippy::unwrap_used, reason = "test code")]
469#[allow(clippy::panic, reason = "test code")]
470mod validate_append_tests {
471    use super::*;
472    use crate::envelope::{PendingEnvelope, pending_envelope};
473
474    fn sk() -> StreamKey {
475        StreamKey::from_slice(b"s")
476    }
477
478    fn batch(envs: &[PendingEnvelope]) -> PendingBatch<'_> {
479        PendingBatch::new(envs).expect("test batches are non-empty by construction")
480    }
481
482    fn env(version: u64) -> PendingEnvelope {
483        pending_envelope(Version::new(version).unwrap())
484            .event_type("E")
485            .payload(b"p".as_slice())
486            .build()
487            .unwrap()
488    }
489
490    #[test]
491    fn fresh_stream_ok() {
492        assert!(validate_append_versions(0, None, batch(&[env(1), env(2), env(3)]), &sk()).is_ok());
493    }
494
495    #[test]
496    fn existing_stream_ok() {
497        assert!(
498            validate_append_versions(5, Version::new(5), batch(&[env(6), env(7)]), &sk()).is_ok()
499        );
500    }
501
502    #[test]
503    fn stale_expected_conflict() {
504        let err =
505            validate_append_versions(5, Version::new(4), batch(&[env(6)]), &sk()).unwrap_err();
506        assert!(matches!(err, AppendValidationError::Conflict { .. }));
507    }
508
509    #[test]
510    fn fresh_stream_with_expected_some_conflict() {
511        // A non-empty expectation on a brand-new stream is a conflict.
512        let err =
513            validate_append_versions(0, Version::new(1), batch(&[env(1)]), &sk()).unwrap_err();
514        assert!(matches!(err, AppendValidationError::Conflict { .. }));
515    }
516
517    #[test]
518    fn gapped_batch_conflict() {
519        let err = validate_append_versions(0, None, batch(&[env(1), env(3)]), &sk()).unwrap_err();
520        match err {
521            AppendValidationError::Conflict {
522                expected, actual, ..
523            } => {
524                assert_eq!(expected, Version::new(2));
525                assert_eq!(actual, Some(env(3).version()));
526            }
527            AppendValidationError::VersionOverflow => {
528                panic!("expected Conflict, got VersionOverflow")
529            }
530        }
531    }
532
533    #[test]
534    fn out_of_order_batch_conflict() {
535        let err = validate_append_versions(0, None, batch(&[env(2), env(1)]), &sk()).unwrap_err();
536        match err {
537            AppendValidationError::Conflict {
538                expected, actual, ..
539            } => {
540                assert_eq!(expected, Version::new(1));
541                assert_eq!(actual, Some(env(2).version()));
542            }
543            AppendValidationError::VersionOverflow => {
544                panic!("expected Conflict, got VersionOverflow")
545            }
546        }
547    }
548
549    #[test]
550    fn wrong_start_version_conflict() {
551        let err =
552            validate_append_versions(5, Version::new(5), batch(&[env(7)]), &sk()).unwrap_err();
553        assert!(matches!(err, AppendValidationError::Conflict { .. }));
554    }
555
556    #[test]
557    fn version_overflow_is_version_overflow_not_conflict() {
558        let err =
559            validate_append_versions(u64::MAX, Version::new(u64::MAX), batch(&[env(1)]), &sk())
560                .unwrap_err();
561        assert!(matches!(err, AppendValidationError::VersionOverflow));
562    }
563}