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}