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}