Skip to main content

mnesis_store/
repository.rs

1// `try_fold` closure bodies clone Arc-wrapped codec/upcast captures from outer
2// scope and re-bind them by the same name — clippy flags as `shadow_reuse`,
3// but the rebinding is idiomatic for per-iteration Arc clones in async
4// combinator chains and renaming everywhere would just add noise.
5#![allow(
6    clippy::shadow_reuse,
7    reason = "per-iteration Arc clones in try_fold closures intentionally re-bind"
8)]
9
10use alloc::sync::Arc;
11use alloc::vec::Vec;
12use core::borrow::Borrow;
13use core::future::Future;
14use core::marker::PhantomData;
15use core::num::NonZeroU32;
16
17use mnesis::{Aggregate, AggregateRoot, DomainEvent, EventOf, Events, Version};
18
19use futures::TryStreamExt;
20
21use crate::codec::{Decode, Encode};
22use crate::envelope::{EnvelopeError, PendingBatch, PersistedEnvelope, pending_envelope};
23use crate::error::{AppendError, LoadWithError, StoreError};
24use crate::metadata::MetadataProvider;
25use crate::store::{AllPosition, RawEventStore, Store};
26use crate::stream_id::StreamKey;
27use crate::upcasting::EventMorsel;
28use crate::value::{Payload, SchemaVersion};
29
30// ═══════════════════════════════════════════════════════════════════════════
31// Repository<A> — high-level aggregate facade (load + save)
32// ═══════════════════════════════════════════════════════════════════════════
33
34/// Port for loading and saving aggregates via event streams.
35///
36/// Implementations handle codec encode/decode, streaming rehydration
37/// via [`AggregateRoot::replay()`], and version tracking internally.
38/// Users interact with aggregates, not envelopes.
39///
40/// # Stream identity
41///
42/// The aggregate's `Id` (via `Aggregate::Id`) is used directly as the
43/// stream identifier. Adapters are responsible for mapping the `Id` to
44/// their internal key format (e.g. string-based key, numeric ID, etc.).
45///
46/// # Streaming Rehydration
47///
48/// `load()` streams events from the store one-by-one through `replay()`,
49/// enabling zero-allocation rehydration with zero-copy codecs (rkyv,
50/// flatbuffers). No intermediate `Vec` allocation is needed.
51///
52/// # Save contract
53///
54/// `save()` takes a mutable reference to the aggregate and the
55/// non-empty [`Events<E, N>`](mnesis::Events) decided by
56/// [`Handle::handle()`](mnesis::Handle::handle). It encodes the events,
57/// appends them atomically using `aggregate.version()` as the expected
58/// version, and on success calls `commit_persisted` to advance the version
59/// and fold the events into in-memory state atomically.
60///
61/// Taking `&Events<E, N>` (not `&[EventOf<A>]`) carries the kernel's
62/// `>= 1` guarantee through to persistence: an empty save is
63/// unrepresentable, so there is no runtime no-op case to guard.
64///
65/// # Schema evolution
66///
67/// The trait surface does not carry an upcaster — `load()` reads events
68/// at their stored schema version and decodes them directly, while `save()`
69/// stamps `Version::INITIAL` as the schema version on each new event. For
70/// schema evolution, drop to the concrete facade and call its inherent
71/// [`load_with`](EventStore::load_with) /
72/// [`save_with`](EventStore::save_with) methods (or compose the
73/// substrate via [`Store::raw`](crate::Store::raw)).
74///
75/// # Error handling
76///
77/// Implementations must bridge errors from four sources:
78/// - [`RawEventStore`](crate::RawEventStore) errors (I/O, conflicts)
79/// - [`Encode`](crate::Encode) errors (serialization failures on write)
80/// - [`Decode`](crate::Decode) errors (deserialization failures on read)
81/// - [`KernelError`](mnesis::KernelError) (version mismatch during replay)
82///
83/// [`StoreError`](crate::StoreError) can represent all four via its
84/// `Adapter`, `Encode`, `Decode`, and `Kernel` variants. Use `StoreError`
85/// as `Self::Error` or define a custom error with `From` impls.
86pub trait Repository<A: Aggregate>: Send + Sync {
87    /// The error type for repository operations.
88    type Error: core::error::Error + Send + Sync + 'static;
89
90    /// The `$all` position [`save`](Self::save) returns — the adapter's
91    /// [`AllPosition`](crate::store::AllPosition), surfaced up from
92    /// [`RawEventStore::append`](crate::RawEventStore::append) (#330).
93    ///
94    /// This is the read-your-writes token: a projection whose checkpoint has
95    /// reached a returned position has necessarily observed the write. On a
96    /// distributed adapter (postgres) the position may be withheld from `$all`
97    /// until a commit watermark clears (#213), so any wait needs a timeout.
98    type Position: AllPosition;
99
100    /// Load an aggregate by replaying its event stream.
101    ///
102    /// Streams events from the store one-by-one through `replay()`,
103    /// enabling zero-allocation rehydration with zero-copy codecs.
104    /// Returns a fresh aggregate at initial state if the stream is empty.
105    fn load(&self, id: A::Id)
106    -> impl Future<Output = Result<AggregateRoot<A>, Self::Error>> + Send;
107
108    /// Persist decided events and advance the aggregate's in-memory state.
109    ///
110    /// `events` is the non-empty [`Events<E, N>`](mnesis::Events) decided by
111    /// [`Handle::handle()`](mnesis::Handle::handle). The aggregate's
112    /// current [`version()`](AggregateRoot::version) is used as the
113    /// expected version for optimistic concurrency.
114    ///
115    /// The `&Events<EventOf<A>, N>` parameter guarantees at least one
116    /// event at compile time — there is no empty-input case.
117    ///
118    /// On success, calls `commit_persisted` with the last persisted version to
119    /// advance the version and fold the events into in-memory state atomically,
120    /// and returns the [`Position`](Self::Position) the last event landed at —
121    /// the read-your-writes token (#330). The advanced version is read off
122    /// `aggregate`; only the position, which the aggregate does not carry, is
123    /// returned (rule 4 — no redundant `(version, position)` pair).
124    fn save<const N: usize>(
125        &self,
126        aggregate: &mut AggregateRoot<A>,
127        events: &Events<EventOf<A>, N>,
128    ) -> impl Future<Output = Result<Self::Position, Self::Error>> + Send;
129}
130
131// ═══════════════════════════════════════════════════════════════════════════
132// ReplayFrom<A> — pub(crate) trait shared with the Snapshotting decorator
133// ═══════════════════════════════════════════════════════════════════════════
134
135/// Internal trait for replaying events from a given starting point.
136///
137/// [`EventStore`] implements this so the
138/// [`Snapshotting`](crate::snapshot::Snapshotting) decorator can share
139/// replay logic. Not public API.
140pub(crate) trait ReplayFrom<A: Aggregate>: Send + Sync {
141    /// The error type for replay operations.
142    type Error: core::error::Error + Send + Sync + 'static;
143
144    /// Replay events starting from `from` version (inclusive) into `root`.
145    ///
146    /// Returns the updated aggregate with all events applied.
147    fn replay_from(
148        &self,
149        root: AggregateRoot<A>,
150        from: Version,
151    ) -> impl Future<Output = Result<AggregateRoot<A>, Self::Error>> + Send;
152}
153
154// ═══════════════════════════════════════════════════════════════════════════
155// Shared helpers
156// ═══════════════════════════════════════════════════════════════════════════
157
158/// Convert a `Version` (`NonZeroU64`) to a `NonZeroU32` for the envelope's
159/// `schema_version` field. Returns `None` if the version exceeds `u32::MAX`.
160pub(super) fn version_to_nz32(version: Version) -> Option<NonZeroU32> {
161    let raw = version.as_u64();
162    let narrow = u32::try_from(raw).ok()?;
163    // SAFETY: Version wraps NonZeroU64, so raw >= 1, so narrow >= 1.
164    NonZeroU32::new(narrow)
165}
166
167/// The first [`Version`] an append will assign, given the stream's current
168/// version (`None` = empty stream). Returns `None` on overflow past `u64::MAX`.
169///
170/// Single source of truth for the "next version to write" computation shared by
171/// the aggregate save paths and the saga repository's intent-version pinning —
172/// keeps the arithmetic checked in exactly one place (CLAUDE.md rule 2).
173pub(crate) const fn first_persisted_version(current: Option<Version>) -> Option<Version> {
174    match current {
175        None => Some(Version::INITIAL),
176        Some(v) => v.next(),
177    }
178}
179
180// ═══════════════════════════════════════════════════════════════════════════
181// EventStore — one facade for any codec (owning or borrowing)
182// ═══════════════════════════════════════════════════════════════════════════
183
184/// Event store over a single [`Encode`] + [`Decode`] codec — one terminal for
185/// both owning and borrowing codecs.
186///
187/// The owning-vs-borrowing distinction is inferred from the codec's
188/// [`Decode::Output`](crate::Decode::Output) GAT, not restated at the call
189/// site: an owning codec (`Output<'a> = E`, e.g. serde — one allocation per
190/// decoded event) and a borrowing codec (`Output<'a> = &'a E`, e.g. a
191/// `#[repr(C)]` POD reinterpret — zero allocation) are unified on the load
192/// path by the bound `Output<'a>: Borrow<E>` (`std`'s `Borrow<T> for T` and
193/// `Borrow<T> for &T` cover both), and the decoded value is fed to
194/// [`replay`](mnesis::AggregateRoot::replay) via `out.borrow()` in either case.
195///
196/// # Construction
197///
198/// Created via [`Store::repository::<A>()`](crate::store::Store::repository),
199/// which names the aggregate `A` once:
200///
201/// ```ignore
202/// let store = Store::new(backend);
203/// let orders = store.repository::<Order>().codec(OrderCodec).build();
204/// let order = orders.load(id).await?;        // AggregateRoot<Order> — inferred
205/// orders.save(&mut order, &events).await?;   // inferred
206/// ```
207///
208/// # Aggregate binding
209///
210/// The aggregate `A` is a phantom type parameter (carried as
211/// `PhantomData<fn() -> A>`, so the facade is `Send + Sync + 'static`
212/// regardless of `A` and stays covariant in it). It exists solely so the
213/// facade implements [`Repository<A>`] for **exactly one** `A`: with `A`
214/// fixed on the type, `load(id)` / `save(..)` infer the aggregate from the
215/// receiver, with no per-call annotation (the blanket-over-`A` impl that
216/// previously defeated inference is gone). `A` is named once, at
217/// `repository::<A>()`. The substrate [`Store<S>`] remains multi-aggregate;
218/// mint one cheap per-aggregate facade per aggregate type.
219///
220/// # Schema evolution
221///
222/// The plain [`load`](Repository::load) / [`save`](Repository::save) path
223/// performs no upcasting. For schema evolution, call
224/// [`load_with`](Self::load_with) with the macro-generated function
225/// (e.g. `OrderTransforms::upcast`) on the read path, and
226/// [`save_with`](Self::save_with) with `OrderTransforms::current_version`
227/// on the write path:
228///
229/// ```ignore
230/// // Read path:
231/// let root = es.load_with(id, OrderTransforms::upcast).await?;
232///
233/// // Write path:
234/// es.save_with(&mut root, &events, OrderTransforms::current_version).await?;
235/// ```
236///
237/// # Internal ownership
238///
239/// Owns the codec as `Arc<C>` and the metadata provider as `Arc<M>` so async
240/// load paths can clone both handles into combinator closures and capture them
241/// by value. Per Rust 2024's stricter capture rules (RFC 3498, rustc issue
242/// 133529), a closure that borrows from `&self` and is then handed to a
243/// `try_fold`-style combinator whose returned future is `+ Send` cannot satisfy
244/// the bound — the future-Send check effectively requires the borrow to be
245/// `'static`. Owning the components via `Arc` and cloning per call sidesteps the
246/// borrow entirely. Cost: one heap allocation at facade construction, one
247/// pointer bump per `load`.
248///
249/// The `M = ()` default keeps every existing call site compiling unchanged; the
250/// inert provider always returns `None` metadata.
251pub struct EventStore<S, C, A, M = ()> {
252    store: Store<S>,
253    codec: Arc<C>,
254    meta: Arc<M>,
255    _aggregate: PhantomData<fn() -> A>,
256}
257
258impl<S, C, A, M> EventStore<S, C, A, M> {
259    /// Create an event store bound to a shared store, codec, and metadata
260    /// provider for aggregate `A`.
261    pub(crate) fn new(store: Store<S>, codec: C, meta: M) -> Self {
262        Self {
263            store,
264            codec: Arc::new(codec),
265            meta: Arc::new(meta),
266            _aggregate: PhantomData,
267        }
268    }
269}
270
271impl<S, C, A, M> ReplayFrom<A> for EventStore<S, C, A, M>
272where
273    A: Aggregate,
274    S: RawEventStore + 'static,
275    for<'a> C: Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
276    EventOf<A>: DomainEvent,
277    S::Stream: Send,
278    M: Send + Sync + 'static,
279{
280    type Error =
281        StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>;
282
283    #[cfg_attr(
284        feature = "tracing",
285        tracing::instrument(
286            name = "mnesis.aggregate.load",
287            level = "debug",
288            skip_all,
289            fields(
290                aggregate = core::any::type_name::<A>(),
291                stream = %root.id(),
292                from = %from,
293                version = tracing::field::Empty
294            )
295        )
296    )]
297    async fn replay_from(
298        &self,
299        root: AggregateRoot<A>,
300        from: Version,
301    ) -> Result<AggregateRoot<A>, Self::Error> {
302        // Clone everything into function-local owned values. The
303        // combinator closure captures the locals (Arc clones), with no
304        // borrow of `&self`. See the doc comment on `EventStore` for the
305        // full Rust 2024 capture-rules rationale.
306        let store = self.store.clone();
307        let codec = Arc::<C>::clone(&self.codec);
308
309        let raw_stream = store
310            .raw()
311            .read_stream(&StreamKey::from_slice(root.id().as_ref()), from)
312            .await
313            .map_err(StoreError::Adapter)?;
314
315        let loaded = raw_stream
316            .map_err(StoreError::Adapter)
317            .try_fold(root, move |mut r, env| {
318                let codec = Arc::<C>::clone(&codec);
319                async move {
320                    let version = env.version();
321                    // `out` is the codec's Output<'a>: either an owned
322                    // `EventOf<A>` or a `&EventOf<A>`. `.borrow()` yields
323                    // `&EventOf<A>` in both arms (std Borrow blanket impls),
324                    // and is consumed in-place by `replay` so it never
325                    // escapes (avoids the GAT `'static` implication).
326                    let out = <C as Decode<EventOf<A>>>::decode(&codec, &env)
327                        .map_err(StoreError::Decode)?;
328                    r.replay(version, out.borrow())?;
329                    Ok(r)
330                }
331            })
332            .await?;
333        #[cfg(feature = "tracing")]
334        tracing::Span::current().record("version", tracing::field::debug(&loaded.version()));
335        Ok(loaded)
336    }
337}
338
339impl<S, C, A, M> Repository<A> for EventStore<S, C, A, M>
340where
341    A: Aggregate,
342    S: RawEventStore + 'static,
343    for<'a> C: Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
344    EventOf<A>: DomainEvent,
345    S::Stream: Send,
346    M: MetadataProvider<EventOf<A>>,
347{
348    type Error =
349        StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>;
350
351    type Position = S::AllPosition;
352
353    async fn load(&self, id: A::Id) -> Result<AggregateRoot<A>, Self::Error> {
354        let root = AggregateRoot::<A>::new(id);
355        self.replay_from(root, Version::INITIAL).await
356    }
357
358    async fn save<const N: usize>(
359        &self,
360        aggregate: &mut AggregateRoot<A>,
361        events: &Events<EventOf<A>, N>,
362    ) -> Result<Self::Position, Self::Error> {
363        // The no-upcaster save stamps Version::INITIAL as the schema
364        // version on every event — the schema-version-lookup function
365        // is only needed when an upcaster is in play. See `save_with`.
366        save_events::<A, S, C, _, M, N>(self, aggregate, events, |_| None).await
367    }
368}
369
370impl<S, C, A, M> EventStore<S, C, A, M> {
371    /// Load an aggregate, running `upcast` over each persisted event
372    /// before decoding it.
373    ///
374    /// `upcast` is the schema-evolution function — typically the
375    /// associated function the `#[mnesis::transforms]` macro emits
376    /// (e.g. `OrderTransforms::upcast`). Pass it directly as a function
377    /// pointer; the `'static` bound on `F` and the `+ Send + Sync` bounds
378    /// are required by the `try_fold` combinator chain (see the doc
379    /// comment on [`EventStore`] for the full Rust 2024 capture-rules
380    /// rationale).
381    ///
382    /// # Errors
383    ///
384    /// Returns [`LoadWithError::Store`] for any non-upcast error
385    /// (adapter, codec, kernel) and [`LoadWithError::Upcast`] for any
386    /// error returned by the `upcast` function.
387    #[allow(
388        clippy::type_complexity,
389        reason = "the four-source LoadWithError return is intrinsic to the contract; an alias would \
390                  hide which domains the upcasting read path can fail from"
391    )]
392    #[cfg_attr(
393        feature = "tracing",
394        tracing::instrument(
395            name = "mnesis.aggregate.load",
396            level = "debug",
397            skip_all,
398            fields(
399                aggregate = core::any::type_name::<A>(),
400                stream = %id,
401                from = %Version::INITIAL,
402                version = tracing::field::Empty
403            )
404        )
405    )]
406    pub async fn load_with<F, E>(
407        &self,
408        id: A::Id,
409        upcast: F,
410    ) -> Result<
411        AggregateRoot<A>,
412        LoadWithError<
413            S::Error,
414            <C as Encode<EventOf<A>>>::Error,
415            <C as Decode<EventOf<A>>>::Error,
416            E,
417        >,
418    >
419    where
420        A: Aggregate,
421        S: RawEventStore + 'static,
422        for<'a> C:
423            Encode<EventOf<A>> + Decode<EventOf<A>, Output<'a>: Borrow<EventOf<A>>> + 'static,
424        F: for<'a> Fn(EventMorsel<'a>) -> Result<EventMorsel<'a>, E> + Send + Sync + 'static,
425        E: core::error::Error + Send + Sync + 'static,
426        EventOf<A>: DomainEvent,
427        S::Stream: Send,
428        M: Send + Sync + 'static,
429    {
430        let store = self.store.clone();
431        let codec = Arc::<C>::clone(&self.codec);
432        let root = AggregateRoot::<A>::new(id);
433
434        let raw_stream = store
435            .raw()
436            .read_stream(&StreamKey::from_slice(root.id().as_ref()), Version::INITIAL)
437            .await
438            .map_err(|e| LoadWithError::Store(StoreError::Adapter(e)))?;
439
440        let upcast = Arc::new(upcast);
441        let loaded = raw_stream
442            .map_err(|e| LoadWithError::Store(StoreError::Adapter(e)))
443            .try_fold(root, move |mut r, env| {
444                let codec = Arc::<C>::clone(&codec);
445                let upcast = Arc::<F>::clone(&upcast);
446                async move {
447                    let version = env.version();
448                    let morsel = EventMorsel::borrowed(
449                        env.event_type(),
450                        env.schema_version_as_version(),
451                        env.payload(),
452                    );
453                    let transformed = upcast(morsel).map_err(LoadWithError::Upcast)?;
454                    // Synthesize a fresh aligned envelope from the transformed
455                    // morsel — the codec's new shape decodes from an envelope,
456                    // not raw bytes, so post-upcast we rebuild the wire row.
457                    let upcast_env = PersistedEnvelope::for_decode(
458                        transformed.event_type(),
459                        transformed.payload(),
460                    )
461                    .map_err(|e| LoadWithError::Store(StoreError::EnvelopeSynthesis(e)))?;
462                    let out = <C as Decode<EventOf<A>>>::decode(&codec, &upcast_env)
463                        .map_err(|e| LoadWithError::Store(StoreError::Decode(e)))?;
464                    r.replay(version, out.borrow())
465                        .map_err(|e| LoadWithError::Store(StoreError::Kernel(e)))?;
466                    Ok(r)
467                }
468            })
469            .await?;
470        #[cfg(feature = "tracing")]
471        tracing::Span::current().record("version", tracing::field::debug(&loaded.version()));
472        Ok(loaded)
473    }
474
475    /// Persist decided events, stamping the schema version on each via
476    /// `current_version`.
477    ///
478    /// `current_version` is typically the associated function the
479    /// `#[mnesis::transforms]` macro emits (e.g.
480    /// `OrderTransforms::current_version`). For event types it doesn't
481    /// know about, it returns `None` and the schema version falls back
482    /// to [`Version::INITIAL`] (the same default as the no-upcaster
483    /// [`save`](Repository::save)).
484    ///
485    /// Returns the [`Position`](Repository::Position) the last event landed at,
486    /// exactly as [`save`](Repository::save) does (#330).
487    ///
488    /// # Errors
489    ///
490    /// The same set of errors [`save`](Repository::save) can produce —
491    /// the schema-version lookup itself is infallible.
492    pub async fn save_with<F, const N: usize>(
493        &self,
494        aggregate: &mut AggregateRoot<A>,
495        events: &Events<EventOf<A>, N>,
496        current_version: F,
497    ) -> Result<
498        S::AllPosition,
499        StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>,
500    >
501    where
502        A: Aggregate,
503        S: RawEventStore + 'static,
504        C: Encode<EventOf<A>> + Decode<EventOf<A>> + 'static,
505        F: Fn(&str) -> Option<Version>,
506        EventOf<A>: DomainEvent,
507        M: MetadataProvider<EventOf<A>>,
508    {
509        save_events::<A, S, C, _, M, N>(self, aggregate, events, current_version).await
510    }
511}
512
513// Single save path shared between Repository::save (no upcaster, always stamps
514// Version::INITIAL) and EventStore::save_with (uses the user's current_version
515// fn). Encode-only — the decode shape is irrelevant on the write path, so this
516// serves owning and borrowing codecs alike.
517#[allow(
518    clippy::type_complexity,
519    reason = "the three-source StoreError return is intrinsic to the contract; an alias would hide \
520              which domains the save path can fail from"
521)]
522#[cfg_attr(
523    feature = "tracing",
524    tracing::instrument(
525        name = "mnesis.aggregate.save",
526        level = "debug",
527        skip_all,
528        fields(
529            aggregate = core::any::type_name::<A>(),
530            stream = %aggregate.id(),
531            events = events.len(),
532            expected = ?aggregate.version(),
533            position = tracing::field::Empty
534        )
535    )
536)]
537async fn save_events<A, S, C, F, M, const N: usize>(
538    facade: &EventStore<S, C, A, M>,
539    aggregate: &mut AggregateRoot<A>,
540    events: &Events<EventOf<A>, N>,
541    current_version: F,
542) -> Result<
543    S::AllPosition,
544    StoreError<S::Error, <C as Encode<EventOf<A>>>::Error, <C as Decode<EventOf<A>>>::Error>,
545>
546where
547    A: Aggregate,
548    S: RawEventStore,
549    C: Encode<EventOf<A>> + Decode<EventOf<A>>,
550    F: Fn(&str) -> Option<Version>,
551    M: MetadataProvider<EventOf<A>>,
552    EventOf<A>: DomainEvent,
553{
554    let expected_version = aggregate.version();
555
556    let mut next_version =
557        first_persisted_version(expected_version).ok_or(StoreError::VersionOverflow)?;
558
559    // Encode head and tail separately so the non-emptiness `Events` guarantees
560    // survives into `PendingBatch` — `from_parts` needs no runtime check and no
561    // unprovable `unwrap` (#330). Scoped in a block so the `current_version`
562    // closure (which is not `Send`) is dropped before the append `.await` —
563    // otherwise the returned future would capture it across the await point and
564    // stop being `Send` (clippy `future_not_send`).
565    let (head, tail, last_version) = {
566        let encode_at = |event: &EventOf<A>, version: Version| {
567            let payload_bytes = <C as Encode<EventOf<A>>>::encode(&facade.codec, event)
568                .map_err(StoreError::Encode)?;
569            let payload = Payload::from_bytes(payload_bytes)
570                .map_err(EnvelopeError::from)
571                .map_err(StoreError::from)?;
572            let schema_version = current_version(event.name()).unwrap_or(Version::INITIAL);
573            let schema_nz32 = version_to_nz32(schema_version).ok_or(StoreError::VersionOverflow)?;
574
575            let metadata = facade.meta.metadata(version, event, &payload);
576
577            let builder = pending_envelope(version)
578                .event(event)
579                .payload(payload.into_bytes())
580                .schema_version(SchemaVersion::new(schema_nz32));
581            match metadata {
582                Some(m) => builder.metadata(m.into_bytes()).build(),
583                None => builder.build(),
584            }
585            .map_err(StoreError::from)
586        };
587
588        let head = encode_at(events.first(), next_version)?;
589        let mut last_version = next_version;
590        let mut tail = Vec::with_capacity(events.rest().len());
591        for event in events.rest() {
592            next_version = next_version.next().ok_or(StoreError::VersionOverflow)?;
593            tail.push(encode_at(event, next_version)?);
594            last_version = next_version;
595        }
596        (head, tail, last_version)
597    };
598
599    let position = facade
600        .store
601        .raw()
602        .append(
603            &StreamKey::from_slice(aggregate.id().as_ref()),
604            expected_version,
605            PendingBatch::from_parts(&head, &tail),
606        )
607        .await
608        .map_err(|err| match err {
609            AppendError::Conflict {
610                stream_id,
611                expected,
612                actual,
613            } => StoreError::Conflict {
614                stream_id,
615                expected,
616                actual,
617            },
618            AppendError::Store(e) => StoreError::Adapter(e),
619        })?;
620
621    #[cfg(feature = "tracing")]
622    tracing::Span::current().record("position", tracing::field::debug(&position));
623
624    aggregate.commit_persisted(last_version, events);
625    Ok(position)
626}
627
628#[cfg(test)]
629mod version_helper_tests {
630    use super::first_persisted_version;
631    use mnesis::Version;
632
633    #[test]
634    fn fresh_stream_starts_at_initial() {
635        assert_eq!(first_persisted_version(None), Some(Version::INITIAL));
636    }
637
638    #[test]
639    fn existing_stream_advances_by_one() {
640        let v = Version::INITIAL;
641        assert_eq!(first_persisted_version(Some(v)), v.next());
642    }
643
644    #[test]
645    fn overflow_at_max_returns_none() {
646        let max = Version::new(u64::MAX).expect("u64::MAX is non-zero");
647        assert_eq!(first_persisted_version(Some(max)), None);
648    }
649}