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
384// ── AtomicAppend trait ────────────────────────────────────────────────────────
385
386/// Extension of [`EventStore`] that atomically appends events **and** enqueues
387/// outbox messages in a single write operation.
388///
389/// Implementations must guarantee that either both the events and the outbox
390/// messages are persisted, or neither is — even across process crashes. For
391/// SlateDB this is achieved via a single `WriteBatch` (requires `slatedb` feature).
392///
393/// # Why a separate trait?
394///
395/// Not every [`EventStore`] backend supports atomic dual-writes (e.g. an
396/// in-memory test store). Keeping atomicity in a separate trait allows
397/// `Process::execute` to work against any `EventStore`, while
398/// `Process::execute_and_enqueue` requires the stronger `AtomicAppend` bound.
399///
400/// # Safety
401///
402/// Only call `append_with_outbox` from the engine's `execute_and_enqueue`
403/// path. Never write events first and outbox messages second — a crash
404/// between the two produces a silent lost APERAK.
405///
406/// `WriteBatch`: see `slatedb::WriteBatch` (requires `slatedb` feature)
407#[allow(async_fn_in_trait)]
408pub trait AtomicAppend: EventStore {
409    /// Atomically append `events` to `stream_id` and schedule `outbox` messages.
410    ///
411    /// The `outbox` slice carries lightweight [`crate::outbox::PendingOutbox`]
412    /// values produced by [`Workflow::handle`]. The implementation is
413    /// responsible for materialising them into fully-typed
414    /// [`crate::outbox::OutboxMessage`] values using the store-assigned fields
415    /// of the stamped envelopes (e.g. `event_id` as `causation_event_id`).
416    ///
417    /// When `outbox` is empty, this degenerates to a plain `EventStore::append`.
418    ///
419    /// # Errors
420    ///
421    /// - [`EngineError::VersionConflict`] — optimistic concurrency check
422    ///   failed; reload state and retry.
423    /// - [`EngineError::Store`] or [`EngineError::Outbox`] — storage failure.
424    ///
425    /// [`Workflow::handle`]: crate::workflow::Workflow::handle
426    #[must_use = "dropping an append_with_outbox Result silently discards a version-conflict or store error"]
427    async fn append_with_outbox(
428        &self,
429        stream_id: &StreamId,
430        expected_version: ExpectedVersion,
431        events: &[NewEvent],
432        outbox: &[crate::outbox::PendingOutbox],
433    ) -> Result<AppendResult, EngineError>;
434
435    /// Atomically append `events`, schedule `outbox` messages, **and** register
436    /// `deadlines` in a single write operation.
437    ///
438    /// Stronger guarantee than calling [`append_with_outbox`] followed by
439    /// [`DeadlineStore::register`]: either all three sets of writes land or
440    /// none do. This eliminates the non-atomic window where a process event is
441    /// persisted but its regulatory deadline is lost (e.g. on a crash between
442    /// the two calls).
443    ///
444    /// # Default implementation
445    ///
446    /// The default falls back to [`append_with_outbox`] only — **deadlines are
447    /// not persisted**. Override this in [`AtomicAppend`] implementations that
448    /// include a deadline store in the same underlying database (e.g.
449    /// `SlateDbStore` (requires `slatedb` feature)) to achieve full atomicity.
450    ///
451    /// Callers using the default must register deadlines separately via
452    /// [`DeadlineStore::register`] after this returns.
453    ///
454    /// # Errors
455    ///
456    /// Same as [`append_with_outbox`].
457    ///
458    /// [`append_with_outbox`]: AtomicAppend::append_with_outbox
459    /// [`DeadlineStore::register`]: crate::deadline::DeadlineStore::register
460    /// `SlateDbStore`: see `crate::store_slatedb` (requires `slatedb` feature)
461    #[must_use = "dropping an append_with_outbox_and_deadlines Result silently discards a version-conflict or store error"]
462    async fn append_with_outbox_and_deadlines(
463        &self,
464        stream_id: &StreamId,
465        expected_version: ExpectedVersion,
466        events: &[NewEvent],
467        outbox: &[crate::outbox::PendingOutbox],
468        _deadlines: &[crate::deadline::Deadline],
469    ) -> Result<AppendResult, EngineError> {
470        // Default: non-atomic fallback — deadlines must be registered separately.
471        self.append_with_outbox(stream_id, expected_version, events, outbox)
472            .await
473    }
474}
475
476// ── InMemoryEventStore ────────────────────────────────────────────────────────
477
478/// Internal state held behind the `Arc<Mutex<…>>`.
479#[cfg(any(test, feature = "testing"))]
480#[derive(Debug, Default)]
481struct InMemoryState {
482    /// Per-stream ordered event log (sequence-number indexed).
483    streams: HashMap<StreamId, Vec<EventEnvelope>>,
484    /// Global insertion-order log across all streams.
485    ///
486    /// This is the source for [`InMemoryEventStore::all_events`]. Keeping a
487    /// separate flat list avoids collecting + sorting across stream-local
488    /// sequence namespaces, which would produce an arbitrary ordering when
489    /// multiple streams are active.
490    global: Vec<EventEnvelope>,
491}
492
493/// A fully in-memory [`EventStore`] for testing and development.
494///
495/// Backed by two logs protected by a `RwLock`:
496/// - A per-stream `HashMap` for sequence-ordered access.
497/// - A global flat `Vec` that preserves cross-stream insertion order.
498///
499/// The store assigns `event_id`, `sequence_number`, `stream_id`, and
500/// `timestamp` to each appended event — callers submit [`NewEvent`] values.
501///
502/// Cloning the store shares the underlying data via `Arc` — all clones see
503/// the same events.
504///
505/// **Not suitable for production.** Use this for:
506/// - Unit and integration tests
507/// - Spikes and local development
508/// - CI environments that must not depend on external services
509///
510/// Only available in `#[cfg(test)]` or with the `testing` feature enabled.
511#[cfg(any(test, feature = "testing"))]
512#[derive(Debug, Default, Clone)]
513pub struct InMemoryEventStore {
514    inner: Arc<RwLock<InMemoryState>>,
515}
516
517#[cfg(any(test, feature = "testing"))]
518impl InMemoryEventStore {
519    /// Create an empty store.
520    #[must_use]
521    pub fn new() -> Self {
522        Self::default()
523    }
524
525    /// Return all events across all streams in insertion order.
526    ///
527    /// Because sequence numbers are stream-local, this method uses the
528    /// insertion-order global log rather than sorting by sequence number.
529    ///
530    /// **Test/development use only.** Production code should use
531    /// [`EventStore::load`] or [`EventStore::load_from`] to read specific
532    /// streams. Loading all events at once from a production store can OOM
533    /// the process.
534    ///
535    /// Available only when the `testing` feature is enabled or in `cfg(test)`.
536    #[cfg(any(test, feature = "testing"))]
537    #[must_use]
538    pub async fn all_events(&self) -> Vec<EventEnvelope> {
539        self.inner.read().await.global.clone()
540    }
541
542    /// Return all events for a specific stream in sequence order.
543    ///
544    /// **Test/development use only.** In production code, prefer
545    /// [`EventStore::load`] which is part of the trait contract.
546    ///
547    /// Available only when the `testing` feature is enabled or in `cfg(test)`.
548    #[cfg(any(test, feature = "testing"))]
549    #[must_use]
550    pub async fn events_for(&self, stream_id: &StreamId) -> Vec<EventEnvelope> {
551        self.inner
552            .read()
553            .await
554            .streams
555            .get(stream_id)
556            .cloned()
557            .unwrap_or_default()
558    }
559}
560
561#[cfg(any(test, feature = "testing"))]
562impl EventStore for InMemoryEventStore {
563    async fn append(
564        &self,
565        stream_id: &StreamId,
566        expected_version: ExpectedVersion,
567        new_events: &[NewEvent],
568    ) -> Result<AppendResult, EngineError> {
569        let mut inner = self.inner.write().await;
570
571        let current = inner.streams.get(stream_id).map_or(0, |s| s.len() as u64);
572
573        // Optimistic concurrency check.
574        match expected_version {
575            ExpectedVersion::NoStream => {
576                if current != 0 {
577                    return Err(EngineError::VersionConflict {
578                        expected: 0,
579                        actual: current,
580                    });
581                }
582            }
583            ExpectedVersion::Exact(v) => {
584                if current != v {
585                    return Err(EngineError::VersionConflict {
586                        expected: v,
587                        actual: current,
588                    });
589                }
590            }
591            ExpectedVersion::Any => {}
592        }
593
594        // Stamp each NewEvent with store-assigned fields.
595        let now = OffsetDateTime::now_utc();
596        let envelopes: Vec<EventEnvelope> = new_events
597            .iter()
598            .enumerate()
599            .map(|(i, new)| {
600                EventEnvelope::from_new(new.clone(), stream_id.clone(), current + i as u64 + 1, now)
601            })
602            .collect();
603
604        // Append to the per-stream log.
605        inner
606            .streams
607            .entry(stream_id.clone())
608            .or_default()
609            .extend_from_slice(&envelopes);
610
611        // Append to the global insertion-order log.
612        inner.global.extend_from_slice(&envelopes);
613
614        Ok(AppendResult {
615            last_sequence: current + new_events.len() as u64,
616            events: envelopes,
617        })
618    }
619
620    async fn load(&self, stream_id: &StreamId) -> Result<Vec<EventEnvelope>, EngineError> {
621        let inner = self.inner.read().await;
622        Ok(inner.streams.get(stream_id).cloned().unwrap_or_default())
623    }
624
625    async fn load_from(
626        &self,
627        stream_id: &StreamId,
628        from_sequence: u64,
629    ) -> Result<Vec<EventEnvelope>, EngineError> {
630        let inner = self.inner.read().await;
631        Ok(inner
632            .streams
633            .get(stream_id)
634            .map(|events| {
635                events
636                    .iter()
637                    .filter(|e| e.sequence_number > from_sequence)
638                    .cloned()
639                    .collect()
640            })
641            .unwrap_or_default())
642    }
643
644    /// O(1) version check — reads the stream length from the HashMap without
645    /// cloning any event payloads.
646    async fn stream_version(&self, stream_id: &StreamId) -> Result<u64, EngineError> {
647        let inner = self.inner.read().await;
648        Ok(inner.streams.get(stream_id).map_or(0, |s| s.len() as u64))
649    }
650
651    /// Returns all known stream identifiers, optionally filtered by `prefix`.
652    ///
653    /// O(n) in the number of streams — scans the HashMap keys once.
654    async fn list_streams(&self, prefix: Option<&str>) -> Result<Vec<StreamId>, EngineError> {
655        let inner = self.inner.read().await;
656        let ids = inner
657            .streams
658            .keys()
659            .filter(|id| prefix.is_none_or(|p| id.as_str().starts_with(p)))
660            .cloned()
661            .collect();
662        Ok(ids)
663    }
664
665    /// Paginated stream enumeration for `InMemoryEventStore`.
666    ///
667    /// Collects all matching keys, sorts them for deterministic order, then
668    /// applies cursor + limit slicing.  O(n) — acceptable for test/dev stores.
669    async fn list_streams_page(
670        &self,
671        prefix: Option<&str>,
672        cursor: Option<&StreamId>,
673        limit: usize,
674    ) -> Result<Vec<StreamId>, EngineError> {
675        if limit == 0 {
676            return Ok(Vec::new());
677        }
678        let inner = self.inner.read().await;
679        let mut ids: Vec<StreamId> = inner
680            .streams
681            .keys()
682            .filter(|id| prefix.is_none_or(|p| id.as_str().starts_with(p)))
683            .cloned()
684            .collect();
685        // Sort for deterministic pagination order (HashMap is unordered).
686        ids.sort_unstable_by(|a, b| a.as_str().cmp(b.as_str()));
687        let iter: Box<dyn Iterator<Item = StreamId>> = match cursor {
688            None => Box::new(ids.into_iter()),
689            Some(c) => Box::new(ids.into_iter().skip_while(move |id| id != c).skip(1)),
690        };
691        Ok(iter.take(limit).collect())
692    }
693
694    /// Fold over events in `stream_id` starting after `from_sequence`
695    /// (exclusive) without materialising the full `Vec<EventEnvelope>`.
696    ///
697    /// In-memory implementation: iterates the in-memory Vec slice.  This is
698    /// O(N) memory but that is acceptable for the in-memory test store
699    /// (production code uses `SlateDbStore::fold_stream` which is cursor-based).
700    async fn fold_stream<T, F>(
701        &self,
702        stream_id: &StreamId,
703        from_sequence: u64,
704        initial: T,
705        mut f: F,
706    ) -> Result<T, EngineError>
707    where
708        T: Send,
709        F: FnMut(T, EventEnvelope) -> Result<T, EngineError> + Send,
710    {
711        let inner = self.inner.read().await;
712        let mut acc = initial;
713        if let Some(events) = inner.streams.get(stream_id) {
714            for env in events.iter().filter(|e| e.sequence_number > from_sequence) {
715                acc = f(acc, env.clone())?;
716            }
717        }
718        Ok(acc)
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    use crate::ids::{ConversationId, CorrelationId, ProcessId, TenantId};
726    use crate::version::WorkflowId;
727
728    fn make_new_event() -> NewEvent {
729        NewEvent {
730            correlation_id: CorrelationId::new(),
731            causation_id: None,
732            conversation_id: ConversationId::new(),
733            process_id: ProcessId::new(),
734            tenant_id: TenantId::new(),
735            workflow_id: WorkflowId::new("test", "FV2024-10-01"),
736            event_type: "TestEvent".into(),
737            schema_version: 1,
738            payload: serde_json::json!({"test": true}),
739        }
740    }
741
742    #[tokio::test]
743    async fn append_and_load_roundtrip() {
744        let store = InMemoryEventStore::new();
745        let stream = StreamId::new("test/s1");
746
747        let result = store
748            .append(
749                &stream,
750                ExpectedVersion::NoStream,
751                &[make_new_event(), make_new_event()],
752            )
753            .await
754            .unwrap();
755
756        assert_eq!(result.events.len(), 2);
757        assert_eq!(result.events[0].sequence_number, 1);
758        assert_eq!(result.events[1].sequence_number, 2);
759        assert_eq!(result.last_sequence, 2);
760
761        let loaded = store.load(&stream).await.unwrap();
762        assert_eq!(loaded.len(), 2);
763    }
764
765    #[tokio::test]
766    async fn store_stamps_stream_id_and_sequence() {
767        let store = InMemoryEventStore::new();
768        let stream = StreamId::new("test/stamp");
769
770        let result = store
771            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
772            .await
773            .unwrap();
774
775        let env = &result.events[0];
776        assert_eq!(env.stream_id, stream);
777        assert_eq!(env.sequence_number, 1);
778    }
779
780    #[tokio::test]
781    async fn version_conflict_is_detected() {
782        let store = InMemoryEventStore::new();
783        let stream = StreamId::new("test/s2");
784
785        store
786            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
787            .await
788            .unwrap();
789
790        let err = store
791            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
792            .await
793            .unwrap_err();
794
795        assert!(matches!(err, EngineError::VersionConflict { .. }));
796    }
797
798    #[tokio::test]
799    async fn load_from_returns_tail_only() {
800        let store = InMemoryEventStore::new();
801        let stream = StreamId::new("test/s3");
802        let events: Vec<_> = (0..5).map(|_| make_new_event()).collect();
803
804        store
805            .append(&stream, ExpectedVersion::NoStream, &events)
806            .await
807            .unwrap();
808
809        let tail = store.load_from(&stream, 3).await.unwrap();
810        assert_eq!(tail.len(), 2, "expected events 4 and 5");
811        assert_eq!(tail[0].sequence_number, 4);
812        assert_eq!(tail[1].sequence_number, 5);
813    }
814
815    #[tokio::test]
816    async fn all_events_preserves_insertion_order_across_streams() {
817        let store = InMemoryEventStore::new();
818        let s1 = StreamId::new("test/order-s1");
819        let s2 = StreamId::new("test/order-s2");
820
821        store
822            .append(&s1, ExpectedVersion::NoStream, &[make_new_event()])
823            .await
824            .unwrap();
825        store
826            .append(&s2, ExpectedVersion::NoStream, &[make_new_event()])
827            .await
828            .unwrap();
829        store
830            .append(&s1, ExpectedVersion::Exact(1), &[make_new_event()])
831            .await
832            .unwrap();
833
834        let all = store.all_events().await;
835        assert_eq!(all.len(), 3);
836        assert_eq!(all[0].stream_id, s1);
837        assert_eq!(all[1].stream_id, s2);
838        assert_eq!(all[2].stream_id, s1);
839    }
840
841    #[tokio::test]
842    async fn arc_wrapper_delegates_correctly() {
843        let store = Arc::new(InMemoryEventStore::new());
844        let stream = StreamId::new("test/arc-s1");
845
846        store
847            .append(&stream, ExpectedVersion::NoStream, &[make_new_event()])
848            .await
849            .unwrap();
850
851        let loaded = store.load(&stream).await.unwrap();
852        assert_eq!(loaded.len(), 1);
853    }
854
855    #[tokio::test]
856    async fn fold_stream_accumulates_without_full_vec() {
857        let store = InMemoryEventStore::new();
858        let stream = StreamId::new("test/fold-s1");
859        let events: Vec<_> = (0..4).map(|_| make_new_event()).collect();
860
861        store
862            .append(&stream, ExpectedVersion::NoStream, &events)
863            .await
864            .unwrap();
865
866        // Fold from the beginning: count events.
867        let count = store
868            .fold_stream(&stream, 0, 0usize, |acc, _| Ok(acc + 1))
869            .await
870            .unwrap();
871        assert_eq!(count, 4);
872
873        // Fold from sequence 2: count only tail events.
874        let tail_count = store
875            .fold_stream(&stream, 2, 0usize, |acc, _| Ok(acc + 1))
876            .await
877            .unwrap();
878        assert_eq!(tail_count, 2);
879    }
880}