kmp_application/commands/
command_application_service.rs1use std::sync::Arc;
2
3#[cfg(test)]
4#[path = "command_application_service_tests.rs"]
5mod tests;
6
7use kmp_domain::{ContextEventStore, ProjectionWriter};
8
9use crate::ApplicationError;
10use crate::commands::{
11 NoopProjectionWriter, UpdateContextCommand, UpdateContextOutcome, UpdateContextUseCase,
12};
13
14#[derive(Debug)]
15pub struct CommandApplicationService<E, W = NoopProjectionWriter> {
16 update_context: Arc<UpdateContextUseCase<E, W>>,
17 projection_access: tokio::sync::RwLock<()>,
20}
21
22impl<E, W> CommandApplicationService<E, W>
23where
24 E: ContextEventStore + Send + Sync,
25 W: ProjectionWriter + Send + Sync,
26{
27 pub fn new(update_context: Arc<UpdateContextUseCase<E, W>>) -> Self {
28 Self {
29 update_context,
30 projection_access: tokio::sync::RwLock::new(()),
31 }
32 }
33
34 pub async fn update_context(
35 &self,
36 command: UpdateContextCommand,
37 ) -> Result<UpdateContextOutcome, ApplicationError> {
38 self.update_context_after_read(command, &std::collections::BTreeMap::new())
39 .await
40 }
41
42 pub(crate) async fn projection_read(&self) -> tokio::sync::RwLockReadGuard<'_, ()> {
43 self.projection_access.read().await
44 }
45
46 pub(crate) async fn memory_revision(&self, about: &str) -> Result<u64, ApplicationError> {
47 self.update_context.memory_revision(about).await
48 }
49
50 pub(crate) async fn update_context_after_read(
54 &self,
55 command: UpdateContextCommand,
56 revisions: &std::collections::BTreeMap<String, u64>,
57 ) -> Result<UpdateContextOutcome, ApplicationError> {
58 let _guard = self.projection_access.write().await;
59 let accepted = match command.idempotency_key.as_deref() {
60 Some(key) => self.accepted_outcome(key).await?.is_some(),
61 None => false,
62 };
63 if !accepted {
64 for (about, revision) in revisions {
65 if self.memory_revision(about).await? != *revision {
66 return Err(ApplicationError::RetryableConflict(
67 "write neighborhood changed during commit; retry the same logical write to refresh it".into(),
68 ));
69 }
70 }
71 }
72 self.update_context
73 .execute_after_read(command, revisions)
74 .await
75 }
76
77 pub async fn accepted_outcome(
79 &self,
80 idempotency_key: &str,
81 ) -> Result<Option<kmp_domain::IdempotentOutcome>, ApplicationError> {
82 self.update_context.accepted_outcome(idempotency_key).await
83 }
84}