Skip to main content

mako_engine/
event_store.rs

1//! [`EventStore`] trait and the in-process `InMemoryEventStore` implementation.
2//!
3//! The engine defines only the trait. The production implementation is
4//! `SlateDbStore` (crate `store_slatedb`), enabled by the `slatedb`
5//! feature flag. `InMemoryEventStore` is included here for tests, spikes, and
6//! development without external dependencies.
7
8use std::sync::Arc;
9
10#[cfg(any(test, feature = "testing"))]
11use std::collections::HashMap;
12#[cfg(any(test, feature = "testing"))]
13use time::OffsetDateTime;
14#[cfg(any(test, feature = "testing"))]
15use tokio::sync::RwLock;
16
17use crate::{
18    envelope::{EventEnvelope, NewEvent},
19    error::EngineError,
20    ids::StreamId,
21};
22
23// ── ExpectedVersion ───────────────────────────────────────────────────────────
24
25/// Optimistic concurrency control contract for [`EventStore::append`].
26///
27/// The caller declares which sequence number they expect the stream to be at.
28/// The store atomically checks this before writing; a mismatch means a
29/// concurrent writer modified the stream and the caller must reload and retry.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ExpectedVersion {
32    /// The stream must not exist yet (sequence number 0).
33    NoStream,
34    /// The stream must be at exactly this sequence number.
35    Exact(u64),
36    /// Skip the concurrency check entirely.
37    ///
38    /// **Do not use in production workflow append paths.** `Any` silently
39    /// accepts any write regardless of concurrent modifications, which can
40    /// produce duplicate or interleaved events in a stream.
41    ///
42    /// Legitimate uses: [`MigrationRunner`] (bulk admin rewrites),
43    /// snapshot-accelerated store internals, and test scaffolding where the
44    /// caller owns all write access by construction.
45    ///
46    /// For normal workflow event appends always use [`ExpectedVersion::NoStream`]
47    /// (first event) or [`ExpectedVersion::Exact`] (subsequent events).
48    ///
49    /// [`MigrationRunner`]: crate::migration::MigrationRunner
50    Any,
51}
52
53// ── AppendResult ──────────────────────────────────────────────────────────────
54
55/// Metadata returned after a successful [`EventStore::append`].
56#[derive(Debug, Clone)]
57pub struct AppendResult {
58    /// The sequence number of the last event written in this batch.
59    pub last_sequence: u64,
60    /// The fully materialised envelopes as persisted by the store.
61    ///
62    /// Each envelope has its `event_id`, `sequence_number`, `stream_id`, and
63    /// `timestamp` stamped by the store. Callers use these for return values
64    /// and projection seeding without re-loading from storage.
65    pub events: Vec<EventEnvelope>,
66}
67
68// ── EventStore trait ──────────────────────────────────────────────────────────
69
70/// Append-only, ordered event stream storage contract.
71///
72/// ## Implementation requirements
73///
74/// - **Ordered**: events within a stream are always returned in append order.
75/// - **Atomic**: a multi-event append either fully succeeds or fully fails.
76/// - **Optimistic concurrency**: detect concurrent writers via
77///   [`ExpectedVersion`].
78/// - **Append-only**: events are never modified or deleted through this API.
79/// - **Sequence number ownership**: the store assigns `sequence_number`,
80///   `event_id`, `stream_id`, and `timestamp` on each appended envelope.
81///   Callers submit [`NewEvent`] values without these fields.
82///
83/// ## Blanket `Arc` implementation
84///
85/// `Arc<S>` implements `EventStore` whenever `S: EventStore`, so
86/// `Process<W, Arc<MyStore>>` works without any extra wrapper type.
87#[allow(async_fn_in_trait)] // RPIT-in-traits with Send bounds require AFIT; use #[allow] until
88// the ecosystem settles on a stable pattern for Rust 1.85 MSRV.
89pub trait EventStore: Send + Sync {
90    /// Atomically append `events` to `stream_id`.
91    ///
92    /// The store assigns `event_id`, `sequence_number`, `stream_id`, and
93    /// `timestamp` on each event. The fully materialised envelopes are
94    /// returned in [`AppendResult::events`].
95    ///
96    /// # Errors
97    ///
98    /// - [`EngineError::VersionConflict`] when `expected_version` is
99    ///   [`ExpectedVersion::NoStream`] or [`ExpectedVersion::Exact`] and the
100    ///   actual stream version does not match.
101    /// - [`EngineError::Store`] for underlying storage failures.
102    #[must_use = "dropping an append Result silently discards a version-conflict or store error"]
103    async fn append(
104        &self,
105        stream_id: &StreamId,
106        expected_version: ExpectedVersion,
107        events: &[NewEvent],
108    ) -> Result<AppendResult, EngineError>;
109
110    /// Load all events from `stream_id` in sequence order.
111    ///
112    /// Returns an empty `Vec` when the stream does not exist.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`EngineError::Store`] for underlying storage failures.
117    #[must_use = "dropping a load Result silently discards a store error"]
118    async fn load(&self, stream_id: &StreamId) -> Result<Vec<EventEnvelope>, EngineError>;
119
120    /// Load events from `stream_id` starting after `from_sequence` (exclusive).
121    ///
122    /// Useful for incremental projection catch-up: pass the projection's last
123    /// processed sequence number to load only new events.
124    ///
125    /// Returns an empty `Vec` when no new events exist.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`EngineError::Store`] for underlying storage failures.
130    #[must_use = "dropping a load_from Result silently discards a store error"]
131    async fn load_from(
132        &self,
133        stream_id: &StreamId,
134        from_sequence: u64,
135    ) -> Result<Vec<EventEnvelope>, EngineError>;
136
137    /// Return the current sequence number of `stream_id`.
138    ///
139    /// The sequence number equals the number of events in the stream (1-based
140    /// after the first append). Returns `0` when the stream does not exist.
141    ///
142    /// Use this instead of `load(…).await?.len()` when you only need the
143    /// count — backends can implement this as a cheap metadata query without
144    /// transferring event payloads.
145    ///
146    /// **Required.** There is no default implementation — a fallback that
147    /// loads all events defeats the O(1) metadata-query contract. Implementors
148    /// must read the stored sequence counter directly (e.g. a `sv/{stream_id}`
149    /// key in SlateDB) to avoid O(n) event-payload transfers.
150    ///
151    /// # Errors
152    ///
153    /// Returns [`EngineError::Store`] for underlying storage failures.
154    #[must_use = "dropping a stream_version Result silently discards a store error"]
155    async fn stream_version(&self, stream_id: &StreamId) -> Result<u64, EngineError>;
156
157    /// Return all known stream identifiers in this store, optionally filtered
158    /// by `prefix`.
159    ///
160    /// When `prefix` is `Some("process/")`, only streams whose identifiers
161    /// start with `"process/"` are returned (e.g. all process-instance
162    /// streams). When `prefix` is `None`, all streams are returned.
163    ///
164    /// This is the primary enumeration API for multi-stream projections: the
165    /// caller discovers all relevant streams, then passes the list to
166    /// [`crate::projection::ProjectionRunner::run_all_streams`] /
167    /// [`crate::projection::ProjectionRunner::catch_up_all_streams`].
168    ///
169    /// The returned order is unspecified. Stable ordering can be achieved by
170    /// sorting the `Vec` before use if deterministic replay is required.
171    ///
172    /// **Required.** There is no default implementation — a missing override
173    /// silently returns no streams, causing multi-stream projections (e.g.
174    /// MABIS billing aggregations) to process zero streams and return empty
175    /// read models with no error signal.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`EngineError::Store`] for underlying storage failures.
180    #[must_use = "dropping a list_streams Result silently discards a store error"]
181    async fn list_streams(&self, prefix: Option<&str>) -> Result<Vec<StreamId>, EngineError>;
182
183    /// Paginated stream enumeration — equivalent to `list_streams` but returns
184    /// at most `limit` entries starting after `cursor` (exclusive, UTF-8
185    /// stream-ID order).
186    ///
187    /// # Parameters
188    ///
189    /// - `prefix` — optional key prefix to restrict the scan (same semantics as
190    ///   `list_streams`).
191    /// - `cursor` — if `Some(s)`, resume after stream ID `s`; `None` starts
192    ///   from the beginning.
193    /// - `limit` — maximum number of stream IDs to return per page.  A return
194    ///   count strictly less than `limit` indicates the last page.
195    ///
196    /// # Page iteration pattern
197    ///
198    /// ```rust,ignore
199    /// let mut cursor: Option<StreamId> = None;
200    /// loop {
201    ///     let page = store.list_streams_page(Some("process/"), cursor.as_ref(), 100).await?;
202    ///     let done = page.len() < 100;
203    ///     for id in &page { /* process */ }
204    ///     cursor = page.into_iter().last();
205    ///     if done { break; }
206    /// }
207    /// ```
208    ///
209    /// # Default implementation
210    ///
211    /// Falls back to `list_streams` + in-memory slicing for stores that do not
212    /// provide a native cursor scan.
213    ///
214    /// # ⚠️ Override required for production stores
215    ///
216    /// This default loads **all** matching stream IDs into memory on every call,
217    /// making `list_streams_page` loops O(n²) in total stream count.  Any
218    /// production `EventStore` implementation (e.g. PostgreSQL, CockroachDB)
219    /// **must** override this method with an efficient cursor-based scan.  The
220    /// SlateDB store already provides such an override.  Failure to override
221    /// this method will cause projection catch-up to degrade silently under
222    /// deployments with > 10,000 active process streams.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`EngineError::Store`] for underlying storage failures.
227    #[must_use = "dropping a list_streams_page Result silently discards a store error"]
228    async fn list_streams_page(
229        &self,
230        prefix: Option<&str>,
231        cursor: Option<&StreamId>,
232        limit: usize,
233    ) -> Result<Vec<StreamId>, EngineError> {
234        // Default: enumerate all + skip-after-cursor + take(limit).
235        let all = self.list_streams(prefix).await?;
236        let iter: Box<dyn Iterator<Item = StreamId>> = match cursor {
237            None => Box::new(all.into_iter()),
238            Some(c) => Box::new(all.into_iter().skip_while(move |id| id != c).skip(1)),
239        };
240        Ok(iter.take(limit).collect())
241    }
242
243    /// Fold over events in `stream_id` starting after `from_sequence`
244    /// (exclusive), accumulating state without materialising the full
245    /// `Vec<EventEnvelope>`.
246    ///
247    /// This is the memory-efficient alternative to `load_from` for large
248    /// streams. Instead of returning all events as a Vec, it applies `f` to
249    /// each event in order and returns the final accumulated value.
250    ///
251    /// `from_sequence = 0` folds from the beginning of the stream.
252    ///
253    /// ```rust,ignore
254    /// // Reconstruct process state event-by-event without a Vec allocation:
255    /// let state = store.fold_stream(
256    ///     &stream_id, 0, W::State::default(),
257    ///     |acc, env| Ok(acc.apply(env.event()))
258    /// ).await?;
259    /// ```
260    ///
261    /// **Required.** There is no default implementation — a fallback that
262    /// materialises `load_from(...)` into a `Vec` defeats the purpose of this
263    /// method for large MABIS billing streams (potentially thousands of events
264    /// per billing period). Implementors must provide a cursor-based scan for
265    /// constant-memory behaviour.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`EngineError::Store`] for underlying storage failures.
270    #[must_use = "dropping a fold_stream Result silently discards a store or fold error"]
271    async fn fold_stream<T, F>(
272        &self,
273        stream_id: &StreamId,
274        from_sequence: u64,
275        initial: T,
276        f: F,
277    ) -> Result<T, EngineError>
278    where
279        T: Send,
280        F: FnMut(T, EventEnvelope) -> Result<T, EngineError> + Send;
281}
282
283// ── Arc<S> blanket impl ───────────────────────────────────────────────────────
284
285impl<S: EventStore> EventStore for Arc<S> {
286    async fn append(
287        &self,
288        stream_id: &StreamId,
289        expected_version: ExpectedVersion,
290        events: &[NewEvent],
291    ) -> Result<AppendResult, EngineError> {
292        self.as_ref()
293            .append(stream_id, expected_version, events)
294            .await
295    }
296
297    async fn load(&self, stream_id: &StreamId) -> Result<Vec<EventEnvelope>, EngineError> {
298        self.as_ref().load(stream_id).await
299    }
300
301    async fn load_from(
302        &self,
303        stream_id: &StreamId,
304        from_sequence: u64,
305    ) -> Result<Vec<EventEnvelope>, EngineError> {
306        self.as_ref().load_from(stream_id, from_sequence).await
307    }
308
309    async fn stream_version(&self, stream_id: &StreamId) -> Result<u64, EngineError> {
310        self.as_ref().stream_version(stream_id).await
311    }
312
313    async fn list_streams(&self, prefix: Option<&str>) -> Result<Vec<StreamId>, EngineError> {
314        self.as_ref().list_streams(prefix).await
315    }
316
317    async fn list_streams_page(
318        &self,
319        prefix: Option<&str>,
320        cursor: Option<&StreamId>,
321        limit: usize,
322    ) -> Result<Vec<StreamId>, EngineError> {
323        self.as_ref().list_streams_page(prefix, cursor, limit).await
324    }
325
326    async fn fold_stream<T, F>(
327        &self,
328        stream_id: &StreamId,
329        from_sequence: u64,
330        initial: T,
331        f: F,
332    ) -> Result<T, EngineError>
333    where
334        T: Send,
335        F: FnMut(T, EventEnvelope) -> Result<T, EngineError> + Send,
336    {
337        self.as_ref()
338            .fold_stream(stream_id, from_sequence, initial, f)
339            .await
340    }
341}
342
343// ── Arc<S>: AtomicAppend blanket impl ────────────────────────────────────────
344
345/// Blanket delegation so `Arc<S>` inherits the `AtomicAppend` contract from
346/// `S`.
347///
348/// This enables `Process<W, Arc<SlateDbStore>>` to call
349/// `execute_and_enqueue` and its retry/snapshot variants without requiring
350/// callers to unwrap the `Arc`.
351impl<S: AtomicAppend> AtomicAppend for Arc<S> {
352    async fn append_with_outbox(
353        &self,
354        stream_id: &StreamId,
355        expected_version: ExpectedVersion,
356        events: &[NewEvent],
357        outbox: &[crate::outbox::PendingOutbox],
358    ) -> Result<AppendResult, EngineError> {
359        self.as_ref()
360            .append_with_outbox(stream_id, expected_version, events, outbox)
361            .await
362    }
363
364    async fn append_with_outbox_and_deadlines(
365        &self,
366        stream_id: &StreamId,
367        expected_version: ExpectedVersion,
368        events: &[NewEvent],
369        outbox: &[crate::outbox::PendingOutbox],
370        deadlines: &[crate::deadline::Deadline],
371    ) -> Result<AppendResult, EngineError> {
372        self.as_ref()
373            .append_with_outbox_and_deadlines(
374                stream_id,
375                expected_version,
376                events,
377                outbox,
378                deadlines,
379            )
380            .await
381    }
382
383    async fn append_with_outbox_deadlines_and_correlations(
384        &self,
385        stream_id: &StreamId,
386        expected_version: ExpectedVersion,
387        events: &[NewEvent],
388        outbox: &[crate::outbox::PendingOutbox],
389        deadlines: &[crate::deadline::Deadline],
390        correlations: &[CorrelationEntry],
391    ) -> Result<AppendResult, EngineError> {
392        self.as_ref()
393            .append_with_outbox_deadlines_and_correlations(
394                stream_id,
395                expected_version,
396                events,
397                outbox,
398                deadlines,
399                correlations,
400            )
401            .await
402    }
403}
404
405// ── AtomicAppend trait ────────────────────────────────────────────────────────
406
407/// Extension of [`EventStore`] that atomically appends events **and** enqueues
408/// outbox messages in a single write operation.
409///
410/// Implementations must guarantee that either both the events and the outbox
411/// messages are persisted, or neither is — even across process crashes. For
412/// SlateDB this is achieved via a single `WriteBatch` (requires `slatedb` feature).
413///
414/// # Why a separate trait?
415///
416/// Not every [`EventStore`] backend supports atomic dual-writes (e.g. an
417/// in-memory test store). Keeping atomicity in a separate trait allows
418/// `Process::execute` to work against any `EventStore`, while
419/// `Process::execute_and_enqueue` requires the stronger `AtomicAppend` bound.
420///
421/// # Safety
422///
423/// Only call `append_with_outbox` from the engine's `execute_and_enqueue`
424/// path. Never write events first and outbox messages second — a crash
425/// between the two produces a silent lost APERAK.
426///
427/// `WriteBatch`: see `slatedb::WriteBatch` (requires `slatedb` feature)
428#[allow(async_fn_in_trait)]
429pub trait AtomicAppend: EventStore {
430    /// Atomically append `events` to `stream_id` and schedule `outbox` messages.
431    ///
432    /// The `outbox` slice carries lightweight [`crate::outbox::PendingOutbox`]
433    /// values produced by [`Workflow::handle`]. The implementation is
434    /// responsible for materialising them into fully-typed
435    /// [`crate::outbox::OutboxMessage`] values using the store-assigned fields
436    /// of the stamped envelopes (e.g. `event_id` as `causation_event_id`).
437    ///
438    /// When `outbox` is empty, this degenerates to a plain `EventStore::append`.
439    ///
440    /// # Errors
441    ///
442    /// - [`EngineError::VersionConflict`] — optimistic concurrency check
443    ///   failed; reload state and retry.
444    /// - [`EngineError::Store`] or [`EngineError::Outbox`] — storage failure.
445    ///
446    /// [`Workflow::handle`]: crate::workflow::Workflow::handle
447    #[must_use = "dropping an append_with_outbox Result silently discards a version-conflict or store error"]
448    async fn append_with_outbox(
449        &self,
450        stream_id: &StreamId,
451        expected_version: ExpectedVersion,
452        events: &[NewEvent],
453        outbox: &[crate::outbox::PendingOutbox],
454    ) -> Result<AppendResult, EngineError>;
455
456    /// Atomically append `events`, schedule `outbox` messages, **and** register
457    /// `deadlines` in a single write operation.
458    ///
459    /// Stronger guarantee than calling [`append_with_outbox`] followed by
460    /// [`DeadlineStore::register`]: either all three sets of writes land or
461    /// none do. This eliminates the non-atomic window where a process event is
462    /// persisted but its regulatory deadline is lost (e.g. on a crash between
463    /// the two calls).
464    ///
465    /// # Default implementation
466    ///
467    /// The default falls back to [`append_with_outbox`] only — **deadlines are
468    /// not persisted**. Override this in [`AtomicAppend`] implementations that
469    /// include a deadline store in the same underlying database (e.g.
470    /// `SlateDbStore` (requires `slatedb` feature)) to achieve full atomicity.
471    ///
472    /// Callers using the default must register deadlines separately via
473    /// [`DeadlineStore::register`] after this returns.
474    ///
475    /// # Errors
476    ///
477    /// Same as [`append_with_outbox`].
478    ///
479    /// [`append_with_outbox`]: AtomicAppend::append_with_outbox
480    /// [`DeadlineStore::register`]: crate::deadline::DeadlineStore::register
481    /// `SlateDbStore`: see `crate::store_slatedb` (requires `slatedb` feature)
482    #[must_use = "dropping an append_with_outbox_and_deadlines Result silently discards a version-conflict or store error"]
483    async fn append_with_outbox_and_deadlines(
484        &self,
485        stream_id: &StreamId,
486        expected_version: ExpectedVersion,
487        events: &[NewEvent],
488        outbox: &[crate::outbox::PendingOutbox],
489        _deadlines: &[crate::deadline::Deadline],
490    ) -> Result<AppendResult, EngineError> {
491        // Default: non-atomic fallback — deadlines must be registered separately.
492        self.append_with_outbox(stream_id, expected_version, events, outbox)
493            .await
494    }
495
496    /// Append events, outbox entries, deadlines **and** correlation-index
497    /// entries in one atomic write.
498    ///
499    /// # Why this exists
500    ///
501    /// A spawn is not complete when its events are durable. Until the business
502    /// key is in the correlation index, nothing can *find* the process: the
503    /// counterparty's reply resolves to no process and is skipped, and the only
504    /// subsequent event is the process's own Frist expiring as a false timeout.
505    /// Registering the key after the append left exactly that window, and the
506    /// failure was warn-only — a crash in between produced a live process that
507    /// was unreachable by business key for the rest of its life, with the
508    /// business key itself blocked against a fresh spawn.
509    ///
510    /// Passing the entries here puts them in the same batch as the events that
511    /// justify them, so a spawn is either wholly visible or wholly absent.
512    ///
513    /// # Default implementation
514    ///
515    /// Falls back to [`append_with_outbox_and_deadlines`] — **correlations are
516    /// not persisted**. Override in implementations whose registry shares the
517    /// underlying database.
518    ///
519    /// # Errors
520    ///
521    /// Same as [`append_with_outbox`].
522    ///
523    /// [`append_with_outbox`]: AtomicAppend::append_with_outbox
524    /// [`append_with_outbox_and_deadlines`]: AtomicAppend::append_with_outbox_and_deadlines
525    #[must_use = "dropping the Result silently discards a version-conflict or store error"]
526    async fn append_with_outbox_deadlines_and_correlations(
527        &self,
528        stream_id: &StreamId,
529        expected_version: ExpectedVersion,
530        events: &[NewEvent],
531        outbox: &[crate::outbox::PendingOutbox],
532        deadlines: &[crate::deadline::Deadline],
533        _correlations: &[CorrelationEntry],
534    ) -> Result<AppendResult, EngineError> {
535        self.append_with_outbox_and_deadlines(
536            stream_id,
537            expected_version,
538            events,
539            outbox,
540            deadlines,
541        )
542        .await
543    }
544}
545
546/// One correlation-index entry: "this business key resolves to this process".
547///
548/// Written in the same batch as the events that created the process — see
549/// [`AtomicAppend::append_with_outbox_deadlines_and_correlations`].
550#[derive(Debug, Clone)]
551pub struct CorrelationEntry {
552    /// Tenant the key is scoped to.
553    pub tenant_id: crate::ids::TenantId,
554    /// The business key — a MaLo, MeLo, Vorgangs- or Belegnummer.
555    pub tag: String,
556    /// Process the key resolves to.
557    pub process_id: crate::ids::ProcessId,
558    /// Full identity, so a lookup can rebuild a `Process` without a second read.
559    pub identity: crate::ids::ProcessIdentity,
560}
561
562// ── InMemoryEventStore ────────────────────────────────────────────────────────
563
564/// Internal state held behind the `Arc<Mutex<…>>`.
565#[cfg(any(test, feature = "testing"))]
566#[derive(Debug, Default)]
567struct InMemoryState {
568    /// Per-stream ordered event log (sequence-number indexed).
569    streams: HashMap<StreamId, Vec<EventEnvelope>>,
570    /// Global insertion-order log across all streams.
571    ///
572    /// This is the source for [`InMemoryEventStore::all_events`]. Keeping a
573    /// separate flat list avoids collecting + sorting across stream-local
574    /// sequence namespaces, which would produce an arbitrary ordering when
575    /// multiple streams are active.
576    global: Vec<EventEnvelope>,
577}
578
579/// A fully in-memory [`EventStore`] for testing and development.
580///
581/// Backed by two logs protected by a `RwLock`:
582/// - A per-stream `HashMap` for sequence-ordered access.
583/// - A global flat `Vec` that preserves cross-stream insertion order.
584///
585/// The store assigns `event_id`, `sequence_number`, `stream_id`, and
586/// `timestamp` to each appended event — callers submit [`NewEvent`] values.
587///
588/// Cloning the store shares the underlying data via `Arc` — all clones see
589/// the same events.
590///
591/// **Not suitable for production.** Use this for:
592/// - Unit and integration tests
593/// - Spikes and local development
594/// - CI environments that must not depend on external services
595///
596/// Only available in `#[cfg(test)]` or with the `testing` feature enabled.
597#[cfg(any(test, feature = "testing"))]
598#[derive(Debug, Default, Clone)]
599pub struct InMemoryEventStore {
600    inner: Arc<RwLock<InMemoryState>>,
601}
602
603#[cfg(any(test, feature = "testing"))]
604impl InMemoryEventStore {
605    /// Create an empty store.
606    #[must_use]
607    pub fn new() -> Self {
608        Self::default()
609    }
610
611    /// Return all events across all streams in insertion order.
612    ///
613    /// Because sequence numbers are stream-local, this method uses the
614    /// insertion-order global log rather than sorting by sequence number.
615    ///
616    /// **Test/development use only.** Production code should use
617    /// [`EventStore::load`] or [`EventStore::load_from`] to read specific
618    /// streams. Loading all events at once from a production store can OOM
619    /// the process.
620    ///
621    /// Available only when the `testing` feature is enabled or in `cfg(test)`.
622    #[cfg(any(test, feature = "testing"))]
623    #[must_use]
624    pub async fn all_events(&self) -> Vec<EventEnvelope> {
625        self.inner.read().await.global.clone()
626    }
627
628    /// Return all events for a specific stream in sequence order.
629    ///
630    /// **Test/development use only.** In production code, prefer
631    /// [`EventStore::load`] which is part of the trait contract.
632    ///
633    /// Available only when the `testing` feature is enabled or in `cfg(test)`.
634    #[cfg(any(test, feature = "testing"))]
635    #[must_use]
636    pub async fn events_for(&self, stream_id: &StreamId) -> Vec<EventEnvelope> {
637        self.inner
638            .read()
639            .await
640            .streams
641            .get(stream_id)
642            .cloned()
643            .unwrap_or_default()
644    }
645}
646
647#[cfg(any(test, feature = "testing"))]
648impl EventStore for InMemoryEventStore {
649    async fn append(
650        &self,
651        stream_id: &StreamId,
652        expected_version: ExpectedVersion,
653        new_events: &[NewEvent],
654    ) -> Result<AppendResult, EngineError> {
655        let mut inner = self.inner.write().await;
656
657        let current = inner.streams.get(stream_id).map_or(0, |s| s.len() as u64);
658
659        // Optimistic concurrency check.
660        match expected_version {
661            ExpectedVersion::NoStream => {
662                if current != 0 {
663                    return Err(EngineError::VersionConflict {
664                        expected: 0,
665                        actual: current,
666                    });
667                }
668            }
669            ExpectedVersion::Exact(v) => {
670                if current != v {
671                    return Err(EngineError::VersionConflict {
672                        expected: v,
673                        actual: current,
674                    });
675                }
676            }
677            ExpectedVersion::Any => {}
678        }
679
680        // Stamp each NewEvent with store-assigned fields.
681        let now = OffsetDateTime::now_utc();
682        let envelopes: Vec<EventEnvelope> = new_events
683            .iter()
684            .enumerate()
685            .map(|(i, new)| {
686                EventEnvelope::from_new(new.clone(), stream_id.clone(), current + i as u64 + 1, now)
687            })
688            .collect();
689
690        // Append to the per-stream log.
691        inner
692            .streams
693            .entry(stream_id.clone())
694            .or_default()
695            .extend_from_slice(&envelopes);
696
697        // Append to the global insertion-order log.
698        inner.global.extend_from_slice(&envelopes);
699
700        Ok(AppendResult {
701            last_sequence: current + new_events.len() as u64,
702            events: envelopes,
703        })
704    }
705
706    async fn load(&self, stream_id: &StreamId) -> Result<Vec<EventEnvelope>, EngineError> {
707        let inner = self.inner.read().await;
708        Ok(inner.streams.get(stream_id).cloned().unwrap_or_default())
709    }
710
711    async fn load_from(
712        &self,
713        stream_id: &StreamId,
714        from_sequence: u64,
715    ) -> Result<Vec<EventEnvelope>, EngineError> {
716        let inner = self.inner.read().await;
717        Ok(inner
718            .streams
719            .get(stream_id)
720            .map(|events| {
721                events
722                    .iter()
723                    .filter(|e| e.sequence_number > from_sequence)
724                    .cloned()
725                    .collect()
726            })
727            .unwrap_or_default())
728    }
729
730    /// O(1) version check — reads the stream length from the HashMap without
731    /// cloning any event payloads.
732    async fn stream_version(&self, stream_id: &StreamId) -> Result<u64, EngineError> {
733        let inner = self.inner.read().await;
734        Ok(inner.streams.get(stream_id).map_or(0, |s| s.len() as u64))
735    }
736
737    /// Returns all known stream identifiers, optionally filtered by `prefix`.
738    ///
739    /// O(n) in the number of streams — scans the HashMap keys once.
740    async fn list_streams(&self, prefix: Option<&str>) -> Result<Vec<StreamId>, EngineError> {
741        let inner = self.inner.read().await;
742        let ids = inner
743            .streams
744            .keys()
745            .filter(|id| prefix.is_none_or(|p| id.as_str().starts_with(p)))
746            .cloned()
747            .collect();
748        Ok(ids)
749    }
750
751    /// Paginated stream enumeration for `InMemoryEventStore`.
752    ///
753    /// Collects all matching keys, sorts them for deterministic order, then
754    /// applies cursor + limit slicing.  O(n) — acceptable for test/dev stores.
755    async fn list_streams_page(
756        &self,
757        prefix: Option<&str>,
758        cursor: Option<&StreamId>,
759        limit: usize,
760    ) -> Result<Vec<StreamId>, EngineError> {
761        if limit == 0 {
762            return Ok(Vec::new());
763        }
764        let inner = self.inner.read().await;
765        let mut ids: Vec<StreamId> = inner
766            .streams
767            .keys()
768            .filter(|id| prefix.is_none_or(|p| id.as_str().starts_with(p)))
769            .cloned()
770            .collect();
771        // Sort for deterministic pagination order (HashMap is unordered).
772        ids.sort_unstable_by(|a, b| a.as_str().cmp(b.as_str()));
773        let iter: Box<dyn Iterator<Item = StreamId>> = match cursor {
774            None => Box::new(ids.into_iter()),
775            Some(c) => Box::new(ids.into_iter().skip_while(move |id| id != c).skip(1)),
776        };
777        Ok(iter.take(limit).collect())
778    }
779
780    /// Fold over events in `stream_id` starting after `from_sequence`
781    /// (exclusive) without materialising the full `Vec<EventEnvelope>`.
782    ///
783    /// In-memory implementation: iterates the in-memory Vec slice.  This is
784    /// O(N) memory but that is acceptable for the in-memory test store
785    /// (production code uses `SlateDbStore::fold_stream` which is cursor-based).
786    async fn fold_stream<T, F>(
787        &self,
788        stream_id: &StreamId,
789        from_sequence: u64,
790        initial: T,
791        mut f: F,
792    ) -> Result<T, EngineError>
793    where
794        T: Send,
795        F: FnMut(T, EventEnvelope) -> Result<T, EngineError> + Send,
796    {
797        let inner = self.inner.read().await;
798        let mut acc = initial;
799        if let Some(events) = inner.streams.get(stream_id) {
800            for env in events.iter().filter(|e| e.sequence_number > from_sequence) {
801                acc = f(acc, env.clone())?;
802            }
803        }
804        Ok(acc)
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811    use crate::ids::{ConversationId, CorrelationId, ProcessId, TenantId};
812    use crate::version::WorkflowId;
813
814    fn make_new_event() -> NewEvent {
815        NewEvent {
816            correlation_id: CorrelationId::new(),
817            causation_id: None,
818            conversation_id: ConversationId::new(),
819            process_id: ProcessId::new(),
820            tenant_id: TenantId::new(),
821            workflow_id: WorkflowId::new("test", "FV2024-10-01"),
822            event_type: "TestEvent".into(),
823            schema_version: 1,
824            payload: serde_json::json!({"test": true}),
825        }
826    }
827
828    #[tokio::test]
829    async fn append_and_load_roundtrip() {
830        let store = InMemoryEventStore::new();
831        let stream = StreamId::new("test/s1");
832
833        let result = store
834            .append(
835                &stream,
836                ExpectedVersion::NoStream,
837                &[make_new_event(), make_new_event()],
838            )
839            .await
840            .unwrap();
841
842        assert_eq!(result.events.len(), 2);
843        assert_eq!(result.events[0].sequence_number, 1);
844        assert_eq!(result.events[1].sequence_number, 2);
845        assert_eq!(result.last_sequence, 2);
846
847        let loaded = store.load(&stream).await.unwrap();
848        assert_eq!(loaded.len(), 2);
849    }
850
851    #[tokio::test]
852    async fn store_stamps_stream_id_and_sequence() {
853        let store = InMemoryEventStore::new();
854        let stream = StreamId::new("test/stamp");
855
856        let result = store
857            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
858            .await
859            .unwrap();
860
861        let env = &result.events[0];
862        assert_eq!(env.stream_id, stream);
863        assert_eq!(env.sequence_number, 1);
864    }
865
866    #[tokio::test]
867    async fn version_conflict_is_detected() {
868        let store = InMemoryEventStore::new();
869        let stream = StreamId::new("test/s2");
870
871        store
872            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
873            .await
874            .unwrap();
875
876        let err = store
877            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
878            .await
879            .unwrap_err();
880
881        assert!(matches!(err, EngineError::VersionConflict { .. }));
882    }
883
884    #[tokio::test]
885    async fn load_from_returns_tail_only() {
886        let store = InMemoryEventStore::new();
887        let stream = StreamId::new("test/s3");
888        let events: Vec<_> = (0..5).map(|_| make_new_event()).collect();
889
890        store
891            .append(&stream, ExpectedVersion::NoStream, &events)
892            .await
893            .unwrap();
894
895        let tail = store.load_from(&stream, 3).await.unwrap();
896        assert_eq!(tail.len(), 2, "expected events 4 and 5");
897        assert_eq!(tail[0].sequence_number, 4);
898        assert_eq!(tail[1].sequence_number, 5);
899    }
900
901    #[tokio::test]
902    async fn all_events_preserves_insertion_order_across_streams() {
903        let store = InMemoryEventStore::new();
904        let s1 = StreamId::new("test/order-s1");
905        let s2 = StreamId::new("test/order-s2");
906
907        store
908            .append(&s1, ExpectedVersion::NoStream, &[make_new_event()])
909            .await
910            .unwrap();
911        store
912            .append(&s2, ExpectedVersion::NoStream, &[make_new_event()])
913            .await
914            .unwrap();
915        store
916            .append(&s1, ExpectedVersion::Exact(1), &[make_new_event()])
917            .await
918            .unwrap();
919
920        let all = store.all_events().await;
921        assert_eq!(all.len(), 3);
922        assert_eq!(all[0].stream_id, s1);
923        assert_eq!(all[1].stream_id, s2);
924        assert_eq!(all[2].stream_id, s1);
925    }
926
927    #[tokio::test]
928    async fn arc_wrapper_delegates_correctly() {
929        let store = Arc::new(InMemoryEventStore::new());
930        let stream = StreamId::new("test/arc-s1");
931
932        store
933            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
934            .await
935            .unwrap();
936
937        let loaded = store.load(&stream).await.unwrap();
938        assert_eq!(loaded.len(), 1);
939    }
940
941    #[tokio::test]
942    async fn fold_stream_accumulates_without_full_vec() {
943        let store = InMemoryEventStore::new();
944        let stream = StreamId::new("test/fold-s1");
945        let events: Vec<_> = (0..4).map(|_| make_new_event()).collect();
946
947        store
948            .append(&stream, ExpectedVersion::NoStream, &events)
949            .await
950            .unwrap();
951
952        // Fold from the beginning: count events.
953        let count = store
954            .fold_stream(&stream, 0, 0usize, |acc, _| Ok(acc + 1))
955            .await
956            .unwrap();
957        assert_eq!(count, 4);
958
959        // Fold from sequence 2: count only tail events.
960        let tail_count = store
961            .fold_stream(&stream, 2, 0usize, |acc, _| Ok(acc + 1))
962            .await
963            .unwrap();
964        assert_eq!(tail_count, 2);
965    }
966}