Skip to main content

evento_core/
executor.rs

1//! Event storage and retrieval abstraction.
2//!
3//! This module defines the [`Executor`] trait, the core abstraction for event
4//! persistence. Implementations handle storing events, querying, and managing
5//! subscriptions.
6//!
7//! # Types
8//!
9//! - [`Executor`] - Core trait for event storage backends
10//! - [`Evento`] - Type-erased wrapper around any executor
11//! - [`EventoGroup`] - Multi-executor aggregation (feature: `group`)
12//! - [`Rw`] - Read-write split executor (feature: `rw`)
13//! - [`ReadAggregator`] - Query filter for reading events
14
15use std::{hash::Hash, sync::Arc};
16use ulid::Ulid;
17
18use crate::{
19    cursor::{Args, ReadResult, Value},
20    Event, RoutingKey, WriteError,
21};
22
23/// Filter for querying events by aggregator.
24///
25/// Use the constructor methods to create filters:
26///
27/// # Example
28///
29/// ```rust,ignore
30/// // All events for an aggregator type
31/// let filter = ReadAggregator::aggregator("myapp/User");
32///
33/// // Events for a specific aggregate instance
34/// let filter = ReadAggregator::id("myapp/User", "user-123");
35///
36/// // Events of a specific type
37/// let filter = ReadAggregator::event("myapp/User", "UserCreated");
38/// ```
39#[derive(Clone, PartialEq, Eq)]
40pub struct ReadAggregator {
41    /// Aggregator type (e.g., "myapp/User")
42    pub aggregator_type: String,
43    /// Optional specific aggregate ID
44    pub aggregator_id: Option<String>,
45    /// Optional event name filter
46    pub name: Option<String>,
47}
48
49impl ReadAggregator {
50    /// Creates a filter with all fields specified.
51    ///
52    /// Filters events by aggregator type, specific aggregate ID, and event name.
53    pub fn new(
54        aggregator_type: impl Into<String>,
55        id: impl Into<String>,
56        name: impl Into<String>,
57    ) -> Self {
58        Self {
59            aggregator_type: aggregator_type.into(),
60            aggregator_id: Some(id.into()),
61            name: Some(name.into()),
62        }
63    }
64
65    /// Creates a filter for all events of an aggregator type.
66    ///
67    /// Returns all events regardless of aggregate ID or event name.
68    pub fn aggregator(value: impl Into<String>) -> Self {
69        Self {
70            aggregator_type: value.into(),
71            aggregator_id: None,
72            name: None,
73        }
74    }
75
76    /// Creates a filter for a specific aggregate instance.
77    ///
78    /// Returns all events for the given aggregator type and ID.
79    pub fn id(aggregator_type: impl Into<String>, id: impl Into<String>) -> Self {
80        Self {
81            aggregator_type: aggregator_type.into(),
82            aggregator_id: Some(id.into()),
83            name: None,
84        }
85    }
86
87    /// Creates a filter for a specific event type.
88    ///
89    /// Returns all events of the given name for an aggregator type.
90    pub fn event(aggregator_type: impl Into<String>, name: impl Into<String>) -> Self {
91        Self {
92            aggregator_type: aggregator_type.into(),
93            aggregator_id: None,
94            name: Some(name.into()),
95        }
96    }
97}
98
99impl Hash for ReadAggregator {
100    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
101        self.aggregator_type.hash(state);
102        self.aggregator_id.hash(state);
103        self.name.hash(state);
104    }
105}
106
107/// Core trait for event storage backends.
108///
109/// Implementations handle persisting events, querying, and managing subscriptions.
110/// The main implementation is [`evento_sql::Sql`](../evento_sql/struct.Sql.html).
111///
112/// # Methods
113///
114/// - `write` - Persist events atomically
115/// - `read` - Query events with filtering and pagination
116/// - `latest_timestamp` - Get the timestamp of the most recent matching event
117/// - `get_subscriber_cursor` - Get subscription position
118/// - `is_subscriber_running` - Check if subscription is active
119/// - `upsert_subscriber` - Create/update subscription
120/// - `acknowledge` - Update subscription cursor
121#[async_trait::async_trait]
122pub trait Executor: Send + Sync + 'static {
123    /// Persists events atomically.
124    ///
125    /// Returns `WriteError::InvalidOriginalVersion` if version conflicts occur.
126    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError>;
127
128    /// Gets the current cursor position for a subscription.
129    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>>;
130
131    /// Checks if a subscription is running with the given worker ID.
132    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool>;
133
134    /// Creates or updates a subscription record.
135    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()>;
136
137    /// Updates subscription cursor after processing events.
138    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()>;
139
140    /// Queries events with filtering and pagination.
141    async fn read(
142        &self,
143        aggregators: Option<Vec<ReadAggregator>>,
144        routing_key: Option<RoutingKey>,
145        args: Args,
146    ) -> anyhow::Result<ReadResult<Event>>;
147
148    /// Returns the timestamp of the most recent event matching the filter.
149    ///
150    /// Returns 0 when no matching event exists. Used by the subscription loop
151    /// to compute lag without fetching the full event row (data/metadata blobs).
152    async fn latest_timestamp(
153        &self,
154        aggregators: Option<Vec<ReadAggregator>>,
155        routing_key: Option<RoutingKey>,
156    ) -> anyhow::Result<u64>;
157
158    /// Retrieves a stored snapshot for an aggregate.
159    ///
160    /// Returns the serialized snapshot data and cursor position, or `None`
161    /// if no snapshot exists for the given aggregate.
162    async fn get_snapshot(
163        &self,
164        aggregator_type: String,
165        aggregator_revision: String,
166        id: String,
167    ) -> anyhow::Result<Option<(Vec<u8>, Value)>>;
168
169    /// Stores a snapshot for an aggregate.
170    ///
171    /// Snapshots cache aggregate state to avoid replaying all events.
172    /// The `cursor` indicates the event position up to which the snapshot is valid.
173    async fn save_snapshot(
174        &self,
175        aggregator_type: String,
176        aggregator_revision: String,
177        id: String,
178        data: Vec<u8>,
179        cursor: Value,
180    ) -> anyhow::Result<()>;
181}
182
183/// Type-erased wrapper around any [`Executor`] implementation.
184///
185/// `Evento` wraps an executor in `Arc<Box<dyn Executor>>` for dynamic dispatch.
186/// This allows storing different executor implementations in the same collection.
187///
188/// # Example
189///
190/// ```rust,ignore
191/// let sql_executor: Sql<sqlx::Sqlite> = pool.into();
192/// let evento = Evento::new(sql_executor);
193///
194/// // Use like any executor
195/// evento.write(events).await?;
196/// ```
197pub struct Evento(Arc<Box<dyn Executor>>);
198
199impl Clone for Evento {
200    fn clone(&self) -> Self {
201        Self(self.0.clone())
202    }
203}
204
205#[async_trait::async_trait]
206impl Executor for Evento {
207    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
208        self.0.write(events).await
209    }
210
211    async fn read(
212        &self,
213        aggregators: Option<Vec<ReadAggregator>>,
214        routing_key: Option<RoutingKey>,
215        args: Args,
216    ) -> anyhow::Result<ReadResult<Event>> {
217        self.0.read(aggregators, routing_key, args).await
218    }
219
220    async fn latest_timestamp(
221        &self,
222        aggregators: Option<Vec<ReadAggregator>>,
223        routing_key: Option<RoutingKey>,
224    ) -> anyhow::Result<u64> {
225        self.0.latest_timestamp(aggregators, routing_key).await
226    }
227
228    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
229        self.0.get_subscriber_cursor(key).await
230    }
231
232    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
233        self.0.is_subscriber_running(key, worker_id).await
234    }
235
236    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
237        self.0.upsert_subscriber(key, worker_id).await
238    }
239
240    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
241        self.0.acknowledge(key, cursor, lag).await
242    }
243
244    async fn get_snapshot(
245        &self,
246        aggregator_type: String,
247        aggregator_revision: String,
248        id: String,
249    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
250        self.0
251            .get_snapshot(aggregator_type, aggregator_revision, id)
252            .await
253    }
254
255    async fn save_snapshot(
256        &self,
257        aggregator_type: String,
258        aggregator_revision: String,
259        id: String,
260        data: Vec<u8>,
261        cursor: Value,
262    ) -> anyhow::Result<()> {
263        self.0
264            .save_snapshot(aggregator_type, aggregator_revision, id, data, cursor)
265            .await
266    }
267}
268
269impl Evento {
270    /// Creates a new type-erased executor wrapper.
271    pub fn new<E: Executor>(executor: E) -> Self {
272        Self(Arc::new(Box::new(executor)))
273    }
274}
275
276/// Multi-executor aggregation (requires `group` feature).
277///
278/// `EventoGroup` combines multiple executors into one. Reads query all executors
279/// and merge results; writes go only to the first executor.
280///
281/// Useful for aggregating events from multiple sources.
282#[cfg(feature = "group")]
283#[derive(Clone, Default)]
284pub struct EventoGroup {
285    executors: Vec<Evento>,
286}
287
288#[cfg(feature = "group")]
289impl EventoGroup {
290    /// Adds an executor to the group.
291    ///
292    /// Returns `self` for method chaining.
293    pub fn executor(mut self, executor: impl Into<Evento>) -> Self {
294        self.executors.push(executor.into());
295
296        self
297    }
298
299    /// Returns a reference to the first executor in the group.
300    ///
301    /// # Panics
302    ///
303    /// Panics if the group has no executors.
304    pub fn first(&self) -> &Evento {
305        self.executors
306            .first()
307            .expect("EventoGroup must have at least one executor")
308    }
309}
310
311#[cfg(feature = "group")]
312#[async_trait::async_trait]
313impl Executor for EventoGroup {
314    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
315        self.first().write(events).await
316    }
317
318    async fn read(
319        &self,
320        aggregators: Option<Vec<ReadAggregator>>,
321        routing_key: Option<RoutingKey>,
322        args: Args,
323    ) -> anyhow::Result<ReadResult<Event>> {
324        use crate::cursor;
325        let futures = self
326            .executors
327            .iter()
328            .map(|e| e.read(aggregators.to_owned(), routing_key.to_owned(), args.clone()));
329
330        let results = futures_util::future::join_all(futures).await;
331        let mut events = vec![];
332        for res in results {
333            for edge in res?.edges {
334                events.push(edge.node);
335            }
336        }
337
338        Ok(cursor::Reader::new(events).args(args).execute()?)
339    }
340
341    async fn latest_timestamp(
342        &self,
343        aggregators: Option<Vec<ReadAggregator>>,
344        routing_key: Option<RoutingKey>,
345    ) -> anyhow::Result<u64> {
346        let futures = self
347            .executors
348            .iter()
349            .map(|e| e.latest_timestamp(aggregators.to_owned(), routing_key.to_owned()));
350
351        let results = futures_util::future::join_all(futures).await;
352        let mut max = 0u64;
353        for res in results {
354            let ts = res?;
355            if ts > max {
356                max = ts;
357            }
358        }
359
360        Ok(max)
361    }
362
363    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
364        self.first().get_subscriber_cursor(key).await
365    }
366
367    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
368        self.first().is_subscriber_running(key, worker_id).await
369    }
370
371    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
372        self.first().upsert_subscriber(key, worker_id).await
373    }
374
375    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
376        self.first().acknowledge(key, cursor, lag).await
377    }
378
379    async fn get_snapshot(
380        &self,
381        aggregator_type: String,
382        aggregator_revision: String,
383        id: String,
384    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
385        self.first()
386            .get_snapshot(aggregator_type, aggregator_revision, id)
387            .await
388    }
389
390    async fn save_snapshot(
391        &self,
392        aggregator_type: String,
393        aggregator_revision: String,
394        id: String,
395        data: Vec<u8>,
396        cursor: Value,
397    ) -> anyhow::Result<()> {
398        self.first()
399            .save_snapshot(aggregator_type, aggregator_revision, id, data, cursor)
400            .await
401    }
402}
403
404/// Read-write split executor (requires `rw` feature).
405///
406/// Separates read and write operations to different executors.
407/// Useful for CQRS patterns where read and write databases differ.
408///
409/// # Example
410///
411/// ```rust,ignore
412/// let rw: Rw<ReadReplica, Primary> = (read_executor, write_executor).into();
413/// ```
414#[cfg(feature = "rw")]
415pub struct Rw<R: Executor, W: Executor> {
416    r: R,
417    w: W,
418}
419
420#[cfg(feature = "rw")]
421impl<R: Executor + Clone, W: Executor + Clone> Clone for Rw<R, W> {
422    fn clone(&self) -> Self {
423        Self {
424            r: self.r.clone(),
425            w: self.w.clone(),
426        }
427    }
428}
429
430#[cfg(feature = "rw")]
431#[async_trait::async_trait]
432impl<R: Executor, W: Executor> Executor for Rw<R, W> {
433    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
434        self.w.write(events).await
435    }
436
437    async fn read(
438        &self,
439        aggregators: Option<Vec<ReadAggregator>>,
440        routing_key: Option<RoutingKey>,
441        args: Args,
442    ) -> anyhow::Result<ReadResult<Event>> {
443        self.r.read(aggregators, routing_key, args).await
444    }
445
446    async fn latest_timestamp(
447        &self,
448        aggregators: Option<Vec<ReadAggregator>>,
449        routing_key: Option<RoutingKey>,
450    ) -> anyhow::Result<u64> {
451        self.r.latest_timestamp(aggregators, routing_key).await
452    }
453
454    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
455        self.r.get_subscriber_cursor(key).await
456    }
457
458    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
459        self.r.is_subscriber_running(key, worker_id).await
460    }
461
462    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
463        self.w.upsert_subscriber(key, worker_id).await
464    }
465
466    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
467        self.w.acknowledge(key, cursor, lag).await
468    }
469
470    async fn get_snapshot(
471        &self,
472        aggregator_type: String,
473        aggregator_revision: String,
474        id: String,
475    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
476        self.r
477            .get_snapshot(aggregator_type, aggregator_revision, id)
478            .await
479    }
480
481    async fn save_snapshot(
482        &self,
483        aggregator_type: String,
484        aggregator_revision: String,
485        id: String,
486        data: Vec<u8>,
487        cursor: Value,
488    ) -> anyhow::Result<()> {
489        self.w
490            .save_snapshot(aggregator_type, aggregator_revision, id, data, cursor)
491            .await
492    }
493}
494
495#[cfg(feature = "rw")]
496impl<R: Executor, W: Executor> From<(R, W)> for Rw<R, W> {
497    fn from((r, w): (R, W)) -> Self {
498        Self { r, w }
499    }
500}