feat: atomic review writes with events

This commit is contained in:
2026-10-26 00:40:43 +01:00
parent 0236a488fa
commit c8a1e8bfe4
19 changed files with 440 additions and 117 deletions

View File

@@ -2,6 +2,7 @@ use async_trait::async_trait;
use crate::{
errors::DomainError,
events::DomainEvent,
models::{
DiaryEntry, DiaryFilter, ExportFormat, FeedEntry, FeedSortBy, FollowingFilter, MovieStats,
Review, ReviewHistory, UserStats, UserTrends,
@@ -42,10 +43,19 @@ pub trait DiaryQuery: Send + Sync {
#[async_trait]
pub trait ReviewRepository: Send + Sync {
async fn save_review(&self, review: &Review) -> Result<(), DomainError>;
async fn save_review(&self, review: &Review, events: &[DomainEvent])
-> Result<(), DomainError>;
async fn get_review_by_id(&self, review_id: &ReviewId) -> Result<Option<Review>, DomainError>;
async fn update_review(&self, review: &Review) -> Result<(), DomainError>;
async fn delete_review(&self, review_id: &ReviewId) -> Result<(), DomainError>;
async fn update_review(
&self,
review: &Review,
events: &[DomainEvent],
) -> Result<(), DomainError>;
async fn delete_review(
&self,
review_id: &ReviewId,
events: &[DomainEvent],
) -> Result<(), DomainError>;
async fn get_all_reviews_for_user(&self, user_id: &UserId) -> Result<Vec<Review>, DomainError>;
}

View File

@@ -538,16 +538,16 @@ pub struct PanicReviewRepository;
#[async_trait]
impl ReviewRepository for PanicReviewRepository {
async fn save_review(&self, _: &Review) -> Result<(), DomainError> {
async fn save_review(&self, _: &Review, _: &[DomainEvent]) -> Result<(), DomainError> {
panic!("PanicReviewRepository called")
}
async fn get_review_by_id(&self, _: &ReviewId) -> Result<Option<Review>, DomainError> {
panic!("PanicReviewRepository called")
}
async fn update_review(&self, _: &Review) -> Result<(), DomainError> {
async fn update_review(&self, _: &Review, _: &[DomainEvent]) -> Result<(), DomainError> {
panic!("PanicReviewRepository called")
}
async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> {
async fn delete_review(&self, _: &ReviewId, _: &[DomainEvent]) -> Result<(), DomainError> {
panic!("PanicReviewRepository called")
}
async fn get_all_reviews_for_user(&self, _: &UserId) -> Result<Vec<Review>, DomainError> {

View File

@@ -21,16 +21,16 @@ pub struct RefusingReviewRepository;
#[async_trait]
impl ReviewRepository for RefusingReviewRepository {
async fn save_review(&self, _: &Review) -> Result<(), DomainError> {
async fn save_review(&self, _: &Review, _: &[DomainEvent]) -> Result<(), DomainError> {
Err(refused())
}
async fn get_review_by_id(&self, _: &ReviewId) -> Result<Option<Review>, DomainError> {
Err(refused())
}
async fn update_review(&self, _: &Review) -> Result<(), DomainError> {
async fn update_review(&self, _: &Review, _: &[DomainEvent]) -> Result<(), DomainError> {
Err(refused())
}
async fn delete_review(&self, _: &ReviewId) -> Result<(), DomainError> {
async fn delete_review(&self, _: &ReviewId, _: &[DomainEvent]) -> Result<(), DomainError> {
Err(refused())
}
async fn get_all_reviews_for_user(&self, _: &UserId) -> Result<Vec<Review>, DomainError> {

View File

@@ -72,6 +72,7 @@ pub struct InMemoryStore {
pub(super) follows: RwLock<Vec<(Uuid, SocialIdentity, FollowState)>>,
pub(super) blocked: RwLock<Vec<(Uuid, SocialIdentity)>>,
pub(super) processed_events: RwLock<HashMap<(Uuid, String), LedgerRow>>,
pub(super) recorded_events: RwLock<Vec<crate::events::DomainEvent>>,
}
impl InMemoryStore {
@@ -94,6 +95,7 @@ impl InMemoryStore {
follows: RwLock::default(),
blocked: RwLock::default(),
processed_events: RwLock::default(),
recorded_events: RwLock::default(),
})
}
@@ -103,6 +105,19 @@ impl InMemoryStore {
store
}
/// Events recorded atomically with a change (the outbox rows the sqlite
/// adapter would have written in the same transaction), in write order.
pub fn recorded_events(&self) -> Vec<crate::events::DomainEvent> {
self.recorded_events.read().unwrap().clone()
}
pub(super) fn record_events(&self, events: &[crate::events::DomainEvent]) {
self.recorded_events
.write()
.unwrap()
.extend_from_slice(events);
}
pub fn movie_count(&self) -> usize {
self.movies.read().unwrap().len()
}

View File

@@ -2,6 +2,7 @@ use async_trait::async_trait;
use crate::{
errors::DomainError,
events::DomainEvent,
models::Review,
ports::ReviewRepository,
value_objects::{ReviewId, UserId},
@@ -11,11 +12,16 @@ use super::InMemoryStore;
#[async_trait]
impl ReviewRepository for InMemoryStore {
async fn save_review(&self, review: &Review) -> Result<(), DomainError> {
async fn save_review(
&self,
review: &Review,
events: &[DomainEvent],
) -> Result<(), DomainError> {
self.reviews
.write()
.unwrap()
.insert(review.id().value(), review.clone());
self.record_events(events);
Ok(())
}
@@ -28,16 +34,26 @@ impl ReviewRepository for InMemoryStore {
.cloned())
}
async fn update_review(&self, review: &Review) -> Result<(), DomainError> {
async fn update_review(
&self,
review: &Review,
events: &[DomainEvent],
) -> Result<(), DomainError> {
self.reviews
.write()
.unwrap()
.insert(review.id().value(), review.clone());
self.record_events(events);
Ok(())
}
async fn delete_review(&self, review_id: &ReviewId) -> Result<(), DomainError> {
async fn delete_review(
&self,
review_id: &ReviewId,
events: &[DomainEvent],
) -> Result<(), DomainError> {
self.reviews.write().unwrap().remove(&review_id.value());
self.record_events(events);
Ok(())
}