Skip to content

Commit a7e755d

Browse files
committed
chore: bump up cqrs-es (0.5) (#10)
## Motivation Track the latest cqrs-es release and keep consumers on a single version. The public `EventSourced` API is preserved; the migration is internal to the `Lifecycle` adapter. Closes RAI-865. ## Solution Migrates the workspace from cqrs-es 0.4.12 to 0.5.0. cqrs-es 0.5 reworks the aggregate command model and drops `async-trait` from its core traits. - `Aggregate::handle` is now `&mut self`, writes events through an `EventSink`, and returns `()`; `fn aggregate_type()` became `const TYPE`. - `Aggregate`, `PersistedEventRepository`, and `ViewRepository` are native `async fn` traits, so `#[async_trait]` is removed from those impls (it stays on `Query` and on our `EventSourced`/`Reactor`). - `Lifecycle::handle` adapts the new signature internally, so consumers' impls of `EventSourced` are untouched. - cqrs-es 0.5 rebuilds snapshots inside `commit` by re-applying handled events; `Lifecycle` rejects a re-applied genesis event, so `handle` leaves `self` at its pre-command state (driving the sink through a throwaway copy) and events apply exactly once. - Verified with `cargo nextest run --workspace` (96 pass), clippy, fmt, and both example crates on cqrs-es 0.5 + sqlx 0.9. Stacked on #9.
1 parent 312d951 commit a7e755d

16 files changed

Lines changed: 406 additions & 196 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ redundant_pub_crate = "allow"
3434

3535
[workspace.dependencies]
3636
async-trait = "0.1.89"
37-
chrono = { version = "0.4.45", features = ["serde"] }
38-
cqrs-es = "0.4.12"
37+
cqrs-es = "0.5.0"
3938
serde = { version = "1.0.228", features = ["derive"] }
4039
serde_json = "1.0.150"
4140
sqlite-es = { path = "crates/sqlite-es" }

crates/event-sorcery/src/lib.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,122 @@ mod tests {
911911
assert_eq!(snapshot_version, 2);
912912
}
913913

914+
#[tokio::test]
915+
async fn snapshot_rebuild_applies_events_exactly_once() {
916+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
917+
struct Tally {
918+
count: u64,
919+
}
920+
921+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
922+
enum TallyEvent {
923+
Started,
924+
Incremented,
925+
}
926+
927+
impl DomainEvent for TallyEvent {
928+
fn event_type(&self) -> String {
929+
format!("{self:?}")
930+
}
931+
932+
fn event_version(&self) -> String {
933+
"1.0".to_string()
934+
}
935+
}
936+
937+
enum TallyCommand {
938+
Start,
939+
Increment,
940+
}
941+
942+
#[async_trait]
943+
impl EventSourced for Tally {
944+
type Id = NumericId;
945+
type Event = TallyEvent;
946+
type Command = TallyCommand;
947+
type Error = WidgetError;
948+
type Services = ();
949+
type Materialized = Nil;
950+
951+
const AGGREGATE_TYPE: &'static str = "Tally";
952+
const PROJECTION: Nil = Nil;
953+
const SCHEMA_VERSION: u64 = 1;
954+
const SNAPSHOT_SIZE: usize = 1;
955+
956+
fn originate(event: &TallyEvent) -> Option<Self> {
957+
match event {
958+
TallyEvent::Started => Some(Self { count: 0 }),
959+
TallyEvent::Incremented => None,
960+
}
961+
}
962+
963+
fn evolve(entity: &Self, event: &TallyEvent) -> Result<Option<Self>, WidgetError> {
964+
match event {
965+
TallyEvent::Started => Ok(None),
966+
TallyEvent::Incremented => Ok(Some(Self {
967+
count: entity.count + 1,
968+
})),
969+
}
970+
}
971+
972+
async fn initialize(
973+
command: TallyCommand,
974+
_services: &(),
975+
) -> Result<Vec<TallyEvent>, WidgetError> {
976+
match command {
977+
TallyCommand::Start => Ok(vec![TallyEvent::Started]),
978+
TallyCommand::Increment => Ok(vec![TallyEvent::Incremented]),
979+
}
980+
}
981+
982+
async fn transition(
983+
&self,
984+
command: TallyCommand,
985+
_services: &(),
986+
) -> Result<Vec<TallyEvent>, WidgetError> {
987+
match command {
988+
TallyCommand::Start => Ok(vec![]),
989+
TallyCommand::Increment => Ok(vec![TallyEvent::Incremented]),
990+
}
991+
}
992+
}
993+
994+
let pool = test_pool().await;
995+
let store = testing::test_store::<Tally>(pool.clone(), ());
996+
997+
// SNAPSHOT_SIZE = 1 forces commit's snapshot rebuild
998+
// (`update_snapshot_with_events`) after every command, exercising the
999+
// re-apply path that requires `Lifecycle::handle` to leave `self` at
1000+
// its pre-command state. A counting entity makes double-application
1001+
// visible as a wrong count rather than a coincidentally-equal value.
1002+
store
1003+
.send(&NumericId(1), TallyCommand::Start)
1004+
.await
1005+
.unwrap();
1006+
store
1007+
.send(&NumericId(1), TallyCommand::Increment)
1008+
.await
1009+
.unwrap();
1010+
store
1011+
.send(&NumericId(1), TallyCommand::Increment)
1012+
.await
1013+
.unwrap();
1014+
1015+
let entity = load_entity::<Tally>(&pool, &NumericId(1)).await.unwrap();
1016+
let tally = entity.expect("tally should exist after three commands");
1017+
assert_eq!(tally.count, 2);
1018+
1019+
let payload: String = sqlx::query_scalar(
1020+
"SELECT payload FROM snapshots \
1021+
WHERE aggregate_type = 'Tally' AND aggregate_id = '1'",
1022+
)
1023+
.fetch_one(&pool)
1024+
.await
1025+
.unwrap();
1026+
let snapshot: serde_json::Value = serde_json::from_str(&payload).unwrap();
1027+
assert_eq!(snapshot, serde_json::json!({"Live": {"count": 2}}));
1028+
}
1029+
9141030
#[tokio::test]
9151031
async fn retain_policy_does_not_compact_events() {
9161032
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

0 commit comments

Comments
 (0)