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    /// Returns a receiver notified after each successful in-process `write`,
140    /// for low-latency subscription wakeup.
141    ///
142    /// The channel carries a monotonically increasing write generation. A
143    /// running subscription selects on this alongside its poll interval so it
144    /// wakes the instant an event is committed through the same executor
145    /// instance (or a clone), instead of waiting for the next poll tick.
146    ///
147    /// Default `None` → pure polling. Cross-process writers are not observed by
148    /// this signal; the poll interval remains the fallback for those.
149    fn write_watch(&self) -> Option<tokio::sync::watch::Receiver<u64>> {
150        None
151    }
152
153    /// An exclusive upper bound, in microseconds since the Unix epoch, on the
154    /// event timestamps a subscription may safely process — or `None` for no
155    /// bound (the default).
156    ///
157    /// Single-store backends append in a total order that matches the
158    /// subscription cursor, so they return `None`. A replicated backend whose
159    /// events can be applied to a node out of cursor order (multi-node Accord)
160    /// returns a stability watermark: the subscription processes only events
161    /// whose timestamp is strictly below it, so it never advances past a
162    /// position where a lower-cursor event could still be applied later. Safety
163    /// holds under the same clock-skew/propagation bound the backend already
164    /// assumes for its own state (e.g. Accord's `compaction_margin`).
165    fn stable_timestamp(&self) -> Option<u64> {
166        None
167    }
168
169    /// Gets the current cursor position for a subscription.
170    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>>;
171
172    /// Checks if a subscription is running with the given worker ID.
173    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool>;
174
175    /// Creates or updates a subscription record.
176    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()>;
177
178    /// Updates subscription cursor after processing events.
179    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()>;
180
181    /// Queries events with filtering and pagination.
182    async fn read(
183        &self,
184        aggregators: Option<Vec<EventFilter>>,
185        routing_key: Option<RoutingKey>,
186        args: Args,
187    ) -> anyhow::Result<ReadResult<Event>>;
188
189    /// Returns the timestamp of the most recent event matching the filter.
190    ///
191    /// Returns 0 when no matching event exists. Used by the subscription loop
192    /// to compute lag without fetching the full event row (data/metadata blobs).
193    async fn latest_timestamp(
194        &self,
195        aggregators: Option<Vec<EventFilter>>,
196        routing_key: Option<RoutingKey>,
197    ) -> anyhow::Result<u64>;
198
199    /// Retrieves a stored snapshot for an aggregate.
200    ///
201    /// Returns the serialized snapshot data and cursor position, or `None`
202    /// if no snapshot exists for the given aggregate.
203    async fn get_snapshot(
204        &self,
205        aggregate_type: String,
206        aggregate_revision: String,
207        id: String,
208    ) -> anyhow::Result<Option<(Vec<u8>, Value)>>;
209
210    /// Stores a snapshot for an aggregate.
211    ///
212    /// Snapshots cache aggregate state to avoid replaying all events.
213    /// The `cursor` indicates the event position up to which the snapshot is valid.
214    async fn save_snapshot(
215        &self,
216        aggregate_type: String,
217        aggregate_revision: String,
218        id: String,
219        data: Vec<u8>,
220        cursor: Value,
221    ) -> anyhow::Result<()>;
222
223    /// Deletes a stored snapshot for an aggregate.
224    ///
225    /// Idempotent: deleting a snapshot that does not exist is not an error.
226    /// Revision is intentionally omitted — `save_snapshot` upserts on
227    /// `(type, id)` so there is only ever one row per aggregate.
228    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()>;
229}
230
231/// Type-erased wrapper around any [`Executor`] implementation.
232///
233/// `Evento` wraps an executor in `Arc<Box<dyn Executor>>` for dynamic dispatch.
234/// This allows storing different executor implementations in the same collection.
235///
236/// # Example
237///
238/// ```rust,ignore
239/// let sql_executor: Sql<sqlx::Sqlite> = pool.into();
240/// let evento = Evento::new(sql_executor);
241///
242/// // Use like any executor
243/// evento.write(events).await?;
244/// ```
245pub struct Evento {
246    inner: Arc<Box<dyn Executor>>,
247    default_routing_key: Option<String>,
248}
249
250impl Clone for Evento {
251    fn clone(&self) -> Self {
252        Self {
253            inner: self.inner.clone(),
254            default_routing_key: self.default_routing_key.clone(),
255        }
256    }
257}
258
259#[async_trait::async_trait]
260impl Executor for Evento {
261    fn default_routing_key(&self) -> Option<&str> {
262        self.default_routing_key.as_deref()
263    }
264
265    async fn write(&self, mut events: Vec<Event>) -> Result<(), WriteError> {
266        if let Some(default) = self.default_routing_key.as_deref() {
267            for event in &mut events {
268                if event.routing_key.is_none() {
269                    event.routing_key = Some(default.to_owned());
270                }
271            }
272        }
273        self.inner.write(events).await
274    }
275
276    fn write_watch(&self) -> Option<tokio::sync::watch::Receiver<u64>> {
277        self.inner.write_watch()
278    }
279
280    fn stable_timestamp(&self) -> Option<u64> {
281        self.inner.stable_timestamp()
282    }
283
284    async fn read(
285        &self,
286        aggregators: Option<Vec<EventFilter>>,
287        routing_key: Option<RoutingKey>,
288        args: Args,
289    ) -> anyhow::Result<ReadResult<Event>> {
290        self.inner.read(aggregators, routing_key, args).await
291    }
292
293    async fn latest_timestamp(
294        &self,
295        aggregators: Option<Vec<EventFilter>>,
296        routing_key: Option<RoutingKey>,
297    ) -> anyhow::Result<u64> {
298        self.inner.latest_timestamp(aggregators, routing_key).await
299    }
300
301    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
302        self.inner.get_subscriber_cursor(key).await
303    }
304
305    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
306        self.inner.is_subscriber_running(key, worker_id).await
307    }
308
309    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
310        self.inner.upsert_subscriber(key, worker_id).await
311    }
312
313    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
314        self.inner.acknowledge(key, cursor, lag).await
315    }
316
317    async fn get_snapshot(
318        &self,
319        aggregate_type: String,
320        aggregate_revision: String,
321        id: String,
322    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
323        self.inner
324            .get_snapshot(aggregate_type, aggregate_revision, id)
325            .await
326    }
327
328    async fn save_snapshot(
329        &self,
330        aggregate_type: String,
331        aggregate_revision: String,
332        id: String,
333        data: Vec<u8>,
334        cursor: Value,
335    ) -> anyhow::Result<()> {
336        self.inner
337            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
338            .await
339    }
340
341    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
342        self.inner.delete_snapshot(aggregate_type, id).await
343    }
344}
345
346impl Evento {
347    /// Creates a new type-erased executor wrapper.
348    pub fn new<E: Executor>(executor: E) -> Self {
349        Self {
350            inner: Arc::new(Box::new(executor)),
351            default_routing_key: None,
352        }
353    }
354
355    /// Sets a default routing key applied to writes and inherited by
356    /// subscriptions built from this executor.
357    ///
358    /// Per-event/per-aggregate routing keys still take precedence on writes.
359    /// Subscriptions inherit this key only when the user has not called
360    /// `.routing_key()` or `.all()`.
361    pub fn default_routing_key(mut self, key: impl Into<String>) -> Self {
362        self.default_routing_key = Some(key.into());
363        self
364    }
365}
366
367/// Multi-executor aggregation (requires `group` feature).
368///
369/// `EventoGroup` combines multiple executors into one. Reads query all executors
370/// and merge results; writes go only to the first executor.
371///
372/// Useful for aggregating events from multiple sources.
373#[cfg(feature = "group")]
374#[derive(Clone, Default)]
375pub struct EventoGroup {
376    executors: Vec<Evento>,
377}
378
379#[cfg(feature = "group")]
380impl EventoGroup {
381    /// Adds an executor to the group.
382    ///
383    /// Returns `self` for method chaining.
384    pub fn executor(mut self, executor: impl Into<Evento>) -> Self {
385        self.executors.push(executor.into());
386
387        self
388    }
389
390    /// Returns a reference to the first executor in the group.
391    ///
392    /// # Panics
393    ///
394    /// Panics if the group has no executors.
395    pub fn first(&self) -> &Evento {
396        self.executors
397            .first()
398            .expect("EventoGroup must have at least one executor")
399    }
400}
401
402#[cfg(feature = "group")]
403#[async_trait::async_trait]
404impl Executor for EventoGroup {
405    fn default_routing_key(&self) -> Option<&str> {
406        self.first().default_routing_key()
407    }
408
409    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
410        self.first().write(events).await
411    }
412
413    fn write_watch(&self) -> Option<tokio::sync::watch::Receiver<u64>> {
414        self.first().write_watch()
415    }
416
417    fn stable_timestamp(&self) -> Option<u64> {
418        self.first().stable_timestamp()
419    }
420
421    async fn read(
422        &self,
423        aggregators: Option<Vec<EventFilter>>,
424        routing_key: Option<RoutingKey>,
425        args: Args,
426    ) -> anyhow::Result<ReadResult<Event>> {
427        use crate::cursor;
428        let futures = self
429            .executors
430            .iter()
431            .map(|e| e.read(aggregators.to_owned(), routing_key.to_owned(), args.clone()));
432
433        let results = futures_util::future::join_all(futures).await;
434        let mut events = vec![];
435        for res in results {
436            for edge in res?.edges {
437                events.push(edge.node);
438            }
439        }
440
441        Ok(cursor::Reader::new(events).args(args).execute()?)
442    }
443
444    async fn latest_timestamp(
445        &self,
446        aggregators: Option<Vec<EventFilter>>,
447        routing_key: Option<RoutingKey>,
448    ) -> anyhow::Result<u64> {
449        let futures = self
450            .executors
451            .iter()
452            .map(|e| e.latest_timestamp(aggregators.to_owned(), routing_key.to_owned()));
453
454        let results = futures_util::future::join_all(futures).await;
455        let mut max = 0u64;
456        for res in results {
457            let ts = res?;
458            if ts > max {
459                max = ts;
460            }
461        }
462
463        Ok(max)
464    }
465
466    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
467        self.first().get_subscriber_cursor(key).await
468    }
469
470    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
471        self.first().is_subscriber_running(key, worker_id).await
472    }
473
474    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
475        self.first().upsert_subscriber(key, worker_id).await
476    }
477
478    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
479        self.first().acknowledge(key, cursor, lag).await
480    }
481
482    async fn get_snapshot(
483        &self,
484        aggregate_type: String,
485        aggregate_revision: String,
486        id: String,
487    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
488        self.first()
489            .get_snapshot(aggregate_type, aggregate_revision, id)
490            .await
491    }
492
493    async fn save_snapshot(
494        &self,
495        aggregate_type: String,
496        aggregate_revision: String,
497        id: String,
498        data: Vec<u8>,
499        cursor: Value,
500    ) -> anyhow::Result<()> {
501        self.first()
502            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
503            .await
504    }
505
506    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
507        self.first().delete_snapshot(aggregate_type, id).await
508    }
509}
510
511/// Read-write split executor (requires `rw` feature).
512///
513/// Separates read and write operations to different executors.
514/// Useful for CQRS patterns where read and write databases differ.
515///
516/// # Example
517///
518/// ```rust,ignore
519/// let rw: Rw<ReadReplica, Primary> = (read_executor, write_executor).into();
520/// ```
521#[cfg(feature = "rw")]
522pub struct Rw<R: Executor, W: Executor> {
523    r: R,
524    w: W,
525}
526
527#[cfg(feature = "rw")]
528impl<R: Executor + Clone, W: Executor + Clone> Clone for Rw<R, W> {
529    fn clone(&self) -> Self {
530        Self {
531            r: self.r.clone(),
532            w: self.w.clone(),
533        }
534    }
535}
536
537#[cfg(feature = "rw")]
538#[async_trait::async_trait]
539impl<R: Executor, W: Executor> Executor for Rw<R, W> {
540    fn default_routing_key(&self) -> Option<&str> {
541        self.w.default_routing_key()
542    }
543
544    async fn write(&self, events: Vec<Event>) -> Result<(), WriteError> {
545        self.w.write(events).await
546    }
547
548    fn write_watch(&self) -> Option<tokio::sync::watch::Receiver<u64>> {
549        self.r.write_watch()
550    }
551
552    fn stable_timestamp(&self) -> Option<u64> {
553        self.r.stable_timestamp()
554    }
555
556    async fn read(
557        &self,
558        aggregators: Option<Vec<EventFilter>>,
559        routing_key: Option<RoutingKey>,
560        args: Args,
561    ) -> anyhow::Result<ReadResult<Event>> {
562        self.r.read(aggregators, routing_key, args).await
563    }
564
565    async fn latest_timestamp(
566        &self,
567        aggregators: Option<Vec<EventFilter>>,
568        routing_key: Option<RoutingKey>,
569    ) -> anyhow::Result<u64> {
570        self.r.latest_timestamp(aggregators, routing_key).await
571    }
572
573    async fn get_subscriber_cursor(&self, key: String) -> anyhow::Result<Option<Value>> {
574        self.r.get_subscriber_cursor(key).await
575    }
576
577    async fn is_subscriber_running(&self, key: String, worker_id: Ulid) -> anyhow::Result<bool> {
578        self.r.is_subscriber_running(key, worker_id).await
579    }
580
581    async fn upsert_subscriber(&self, key: String, worker_id: Ulid) -> anyhow::Result<()> {
582        self.w.upsert_subscriber(key, worker_id).await
583    }
584
585    async fn acknowledge(&self, key: String, cursor: Value, lag: u64) -> anyhow::Result<()> {
586        self.w.acknowledge(key, cursor, lag).await
587    }
588
589    async fn get_snapshot(
590        &self,
591        aggregate_type: String,
592        aggregate_revision: String,
593        id: String,
594    ) -> anyhow::Result<Option<(Vec<u8>, Value)>> {
595        self.r
596            .get_snapshot(aggregate_type, aggregate_revision, id)
597            .await
598    }
599
600    async fn save_snapshot(
601        &self,
602        aggregate_type: String,
603        aggregate_revision: String,
604        id: String,
605        data: Vec<u8>,
606        cursor: Value,
607    ) -> anyhow::Result<()> {
608        self.w
609            .save_snapshot(aggregate_type, aggregate_revision, id, data, cursor)
610            .await
611    }
612
613    async fn delete_snapshot(&self, aggregate_type: String, id: String) -> anyhow::Result<()> {
614        self.w.delete_snapshot(aggregate_type, id).await
615    }
616}
617
618#[cfg(feature = "rw")]
619impl<R: Executor, W: Executor> From<(R, W)> for Rw<R, W> {
620    fn from((r, w): (R, W)) -> Self {
621        Self { r, w }
622    }
623}