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//! - [`EventFilter`] - 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 aggregate.
24///
25/// Use the constructor methods to create filters:
26///
27/// # Example
28///
29/// ```rust,ignore
30/// // All events for an aggregate type
31/// let filter = EventFilter::by_type("myapp/User");
32///
33/// // Events for a specific aggregate instance
34/// let filter = EventFilter::by_id("myapp/User", "user-123");
35///
36/// // Events of a specific type
37/// let filter = EventFilter::by_event("myapp/User", "UserCreated");
38/// ```
39#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub struct EventFilter {
41    /// Aggregate type (e.g., "myapp/User")
42    pub aggregate_type: String,
43    /// Optional specific aggregate ID
44    pub aggregate_id: Option<String>,
45    /// Optional event name filter
46    pub name: Option<String>,
47}
48
49impl EventFilter {
50    /// Creates a filter with all fields specified.
51    ///
52    /// Filters events by aggregate type, specific aggregate ID, and event name.
53    pub fn exact(
54        aggregate_type: impl Into<String>,
55        id: impl Into<String>,
56        name: impl Into<String>,
57    ) -> Self {
58        Self {
59            aggregate_type: aggregate_type.into(),
60            aggregate_id: Some(id.into()),
61            name: Some(name.into()),
62        }
63    }
64
65    /// Creates a filter for all events of an aggregate type.
66    ///
67    /// Returns all events regardless of aggregate ID or event name.
68    pub fn by_type(value: impl Into<String>) -> Self {
69        Self {
70            aggregate_type: value.into(),
71            aggregate_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 aggregate type and ID.
79    pub fn by_id(aggregate_type: impl Into<String>, id: impl Into<String>) -> Self {
80        Self {
81            aggregate_type: aggregate_type.into(),
82            aggregate_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 aggregate type.
90    pub fn by_event(aggregate_type: impl Into<String>, name: impl Into<String>) -> Self {
91        Self {
92            aggregate_type: aggregate_type.into(),
93            aggregate_id: None,
94            name: Some(name.into()),
95        }
96    }
97}
98
99impl Hash for EventFilter {
100    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
101        self.aggregate_type.hash(state);
102        self.aggregate_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    /// Default routing key applied to writes and inherited by subscriptions.
124    ///
125    /// Most backends return `None`; [`Evento`] overrides this to expose its
126    /// configured default. Used by `Evento::write` to fill in missing routing
127    /// keys, and by `SubscriptionBuilder::start` /
128    /// `ProjectionSubscription::start` to inherit a default when the user
129    /// has not called `.routing_key()` or `.all()`.
130    fn default_routing_key(&self) -> Option<&str> {
131        None
132    }
133
134    /// Persists events atomically.
135    ///
136    /// Returns `WriteError::InvalidOriginalVersion` if version conflicts occur.
137    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError>;
138
139    /// Gets the current cursor position for a subscription.
140    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>>;
141
142    /// Checks if a subscription is running with the given worker ID.
143    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool>;
144
145    /// Creates or updates a subscription record.
146    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()>;
147
148    /// Updates subscription cursor after processing events.
149    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()>;
150
151    /// Queries events with filtering and pagination.
152    async fn read(
153        &self,
154        aggregators: Option<Vec<EventFilter>>,
155        routing_key: Option<RoutingKey>,
156        args: Args,
157    ) -> anyhow::Result<ReadResult<Event>>;
158
159    /// Returns the timestamp of the most recent event matching the filter.
160    ///
161    /// Returns 0 when no matching event exists. Used by the subscription loop
162    /// to compute lag without fetching the full event row (data/metadata blobs).
163    async fn latest_timestamp(
164        &self,
165        aggregators: Option<Vec<EventFilter>>,
166        routing_key: Option<RoutingKey>,
167    ) -> anyhow::Result<u64>;
168
169    /// Retrieves a stored snapshot for an aggregate.
170    ///
171    /// Returns the serialized snapshot data and cursor position, or `None`
172    /// if no snapshot exists for the given aggregate.
173    async fn get_snapshot(
174        &self,
175        aggregate_type: String,
176        aggregate_revision: String,
177        id: String,
178    ) -> anyhow::Result<Option<(Vec<u8>, Value)>>;
179
180    /// Stores a snapshot for an aggregate.
181    ///
182    /// Snapshots cache aggregate state to avoid replaying all events.
183    /// The `cursor` indicates the event position up to which the snapshot is valid.
184    async fn save_snapshot(
185        &self,
186        aggregate_type: String,
187        aggregate_revision: String,
188        id: String,
189        data: Vec<u8>,
190        cursor: Value,
191    ) -> anyhow::Result<()>;
192
193    /// Deletes a stored snapshot for an aggregate.
194    ///
195    /// Idempotent: deleting a snapshot that does not exist is not an error.
196    /// Revision is intentionally omitted — `save_snapshot` upserts on
197    /// `(type, id)` so there is only ever one row per aggregate.
198    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()>;
199}
200
201/// Type-erased wrapper around any [`Executor`] implementation.
202///
203/// `Evento` wraps an executor in `Arc<Box<dyn Executor>>` for dynamic dispatch.
204/// This allows storing different executor implementations in the same collection.
205///
206/// # Example
207///
208/// ```rust,ignore
209/// let sql_executor: Sql<sqlx::Sqlite> = pool.into();
210/// let evento = Evento::new(sql_executor);
211///
212/// // Use like any executor
213/// evento.write(events).await?;
214/// ```
215pub struct Evento {
216    inner: Arc<Box<dyn Executor>>,
217    default_routing_key: Option<String>,
218}
219
220impl Clone for Evento {
221    fn clone(&self) -> Self {
222        Self {
223            inner: self.inner.clone(),
224            default_routing_key: self.default_routing_key.clone(),
225        }
226    }
227}
228
229#[async_trait::async_trait]
230impl Executor for Evento {
231    fn default_routing_key(&self) -> Option<&str> {
232        self.default_routing_key.as_deref()
233    }
234
235    async fn write(&self, mut events: Vec<Event>) -> Result<(), WriteError> {
236        if let Some(default) = self.default_routing_key.as_deref() {
237            for event in &mut events {
238                if event.routing_key.is_none() {
239                    event.routing_key = Some(default.to_owned());
240                }
241            }
242        }
243        self.inner.write(events).await
244    }
245
246    async fn read(
247        &self,
248        aggregators: Option<Vec<EventFilter>>,
249        routing_key: Option<RoutingKey>,
250        args: Args,
251    ) -> anyhow::Result<ReadResult<Event>> {
252        self.inner.read(aggregators, routing_key, args).await
253    }
254
255    async fn latest_timestamp(
256        &self,
257        aggregators: Option<Vec<EventFilter>>,
258        routing_key: Option<RoutingKey>,
259    ) -> anyhow::Result<u64> {
260        self.inner.latest_timestamp(aggregators, routing_key).await
261    }
262
263    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
264        self.inner.get_subscriber_cursor(key).await
265    }
266
267    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
268        self.inner.is_subscriber_running(key, worker_id).await
269    }
270
271    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
272        self.inner.upsert_subscriber(key, worker_id).await
273    }
274
275    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
276        self.inner.acknowledge(key, cursor, lag).await
277    }
278
279    async fn get_snapshot(
280        &self,
281        aggregate_type: String,
282        aggregate_revision: String,
283        id: String,
284    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
285        self.inner
286            .get_snapshot(aggregate_type, aggregate_revision, id)
287            .await
288    }
289
290    async fn save_snapshot(
291        &self,
292        aggregate_type: String,
293        aggregate_revision: String,
294        id: String,
295        data: Vec<u8>,
296        cursor: Value,
297    ) -> anyhow::Result<()> {
298        self.inner
299            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
300            .await
301    }
302
303    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
304        self.inner.delete_snapshot(aggregate_type, id).await
305    }
306}
307
308impl Evento {
309    /// Creates a new type-erased executor wrapper.
310    pub fn new<E: Executor>(executor: E) -> Self {
311        Self {
312            inner: Arc::new(Box::new(executor)),
313            default_routing_key: None,
314        }
315    }
316
317    /// Sets a default routing key applied to writes and inherited by
318    /// subscriptions built from this executor.
319    ///
320    /// Per-event/per-aggregate routing keys still take precedence on writes.
321    /// Subscriptions inherit this key only when the user has not called
322    /// `.routing_key()` or `.all()`.
323    pub fn default_routing_key(mut self, key: impl Into<String>) -> Self {
324        self.default_routing_key = Some(key.into());
325        self
326    }
327}
328
329/// Multi-executor aggregation (requires `group` feature).
330///
331/// `EventoGroup` combines multiple executors into one. Reads query all executors
332/// and merge results; writes go only to the first executor.
333///
334/// Useful for aggregating events from multiple sources.
335#[cfg(feature = "group")]
336#[derive(Clone, Default)]
337pub struct EventoGroup {
338    executors: Vec<Evento>,
339}
340
341#[cfg(feature = "group")]
342impl EventoGroup {
343    /// Adds an executor to the group.
344    ///
345    /// Returns `self` for method chaining.
346    pub fn executor(mut self, executor: impl Into<Evento>) -> Self {
347        self.executors.push(executor.into());
348
349        self
350    }
351
352    /// Returns a reference to the first executor in the group.
353    ///
354    /// # Panics
355    ///
356    /// Panics if the group has no executors.
357    pub fn first(&self) -> &Evento {
358        self.executors
359            .first()
360            .expect("EventoGroup must have at least one executor")
361    }
362}
363
364#[cfg(feature = "group")]
365#[async_trait::async_trait]
366impl Executor for EventoGroup {
367    fn default_routing_key(&self) -> Option<&str> {
368        self.first().default_routing_key()
369    }
370
371    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
372        self.first().write(events).await
373    }
374
375    async fn read(
376        &self,
377        aggregators: Option<Vec<EventFilter>>,
378        routing_key: Option<RoutingKey>,
379        args: Args,
380    ) -> anyhow::Result<ReadResult<Event>> {
381        use crate::cursor;
382        let futures = self
383            .executors
384            .iter()
385            .map(|e| e.read(aggregators.to_owned(), routing_key.to_owned(), args.clone()));
386
387        let results = futures_util::future::join_all(futures).await;
388        let mut events = vec![];
389        for res in results {
390            for edge in res?.edges {
391                events.push(edge.node);
392            }
393        }
394
395        Ok(cursor::Reader::new(events).args(args).execute()?)
396    }
397
398    async fn latest_timestamp(
399        &self,
400        aggregators: Option<Vec<EventFilter>>,
401        routing_key: Option<RoutingKey>,
402    ) -> anyhow::Result<u64> {
403        let futures = self
404            .executors
405            .iter()
406            .map(|e| e.latest_timestamp(aggregators.to_owned(), routing_key.to_owned()));
407
408        let results = futures_util::future::join_all(futures).await;
409        let mut max = 0u64;
410        for res in results {
411            let ts = res?;
412            if ts > max {
413                max = ts;
414            }
415        }
416
417        Ok(max)
418    }
419
420    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
421        self.first().get_subscriber_cursor(key).await
422    }
423
424    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
425        self.first().is_subscriber_running(key, worker_id).await
426    }
427
428    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
429        self.first().upsert_subscriber(key, worker_id).await
430    }
431
432    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
433        self.first().acknowledge(key, cursor, lag).await
434    }
435
436    async fn get_snapshot(
437        &self,
438        aggregate_type: String,
439        aggregate_revision: String,
440        id: String,
441    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
442        self.first()
443            .get_snapshot(aggregate_type, aggregate_revision, id)
444            .await
445    }
446
447    async fn save_snapshot(
448        &self,
449        aggregate_type: String,
450        aggregate_revision: String,
451        id: String,
452        data: Vec<u8>,
453        cursor: Value,
454    ) -> anyhow::Result<()> {
455        self.first()
456            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
457            .await
458    }
459
460    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
461        self.first().delete_snapshot(aggregate_type, id).await
462    }
463}
464
465/// Read-write split executor (requires `rw` feature).
466///
467/// Separates read and write operations to different executors.
468/// Useful for CQRS patterns where read and write databases differ.
469///
470/// # Example
471///
472/// ```rust,ignore
473/// let rw: Rw<ReadReplica, Primary> = (read_executor, write_executor).into();
474/// ```
475#[cfg(feature = "rw")]
476pub struct Rw<R: Executor, W: Executor> {
477    r: R,
478    w: W,
479}
480
481#[cfg(feature = "rw")]
482impl<R: Executor + Clone, W: Executor + Clone> Clone for Rw<R, W> {
483    fn clone(&self) -> Self {
484        Self {
485            r: self.r.clone(),
486            w: self.w.clone(),
487        }
488    }
489}
490
491#[cfg(feature = "rw")]
492#[async_trait::async_trait]
493impl<R: Executor, W: Executor> Executor for Rw<R, W> {
494    fn default_routing_key(&self) -> Option<&str> {
495        self.w.default_routing_key()
496    }
497
498    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
499        self.w.write(events).await
500    }
501
502    async fn read(
503        &self,
504        aggregators: Option<Vec<EventFilter>>,
505        routing_key: Option<RoutingKey>,
506        args: Args,
507    ) -> anyhow::Result<ReadResult<Event>> {
508        self.r.read(aggregators, routing_key, args).await
509    }
510
511    async fn latest_timestamp(
512        &self,
513        aggregators: Option<Vec<EventFilter>>,
514        routing_key: Option<RoutingKey>,
515    ) -> anyhow::Result<u64> {
516        self.r.latest_timestamp(aggregators, routing_key).await
517    }
518
519    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
520        self.r.get_subscriber_cursor(key).await
521    }
522
523    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
524        self.r.is_subscriber_running(key, worker_id).await
525    }
526
527    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
528        self.w.upsert_subscriber(key, worker_id).await
529    }
530
531    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
532        self.w.acknowledge(key, cursor, lag).await
533    }
534
535    async fn get_snapshot(
536        &self,
537        aggregate_type: String,
538        aggregate_revision: String,
539        id: String,
540    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
541        self.r
542            .get_snapshot(aggregate_type, aggregate_revision, id)
543            .await
544    }
545
546    async fn save_snapshot(
547        &self,
548        aggregate_type: String,
549        aggregate_revision: String,
550        id: String,
551        data: Vec<u8>,
552        cursor: Value,
553    ) -> anyhow::Result<()> {
554        self.w
555            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
556            .await
557    }
558
559    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
560        self.w.delete_snapshot(aggregate_type, id).await
561    }
562}
563
564#[cfg(feature = "rw")]
565impl<R: Executor, W: Executor> From<(R, W)> for Rw<R, W> {
566    fn from((r, w): (R, W)) -> Self {
567        Self { r, w }
568    }
569}