1use std::{
2 collections::BTreeMap,
3 sync::{Arc, Mutex, MutexGuard},
4};
5
6use runifold_core::{CapabilityId, EffectId};
7
8use crate::{EffectExecutorError, EffectExecutorErrorKind, EffectRecord};
9
10pub trait EffectStore: Send + Sync {
12 fn load(&self, id: EffectId) -> Result<Option<EffectRecord>, EffectExecutorError>;
18
19 fn find_by_idempotency(
25 &self,
26 capability_id: CapabilityId,
27 key: &str,
28 ) -> Result<Option<EffectRecord>, EffectExecutorError>;
29
30 fn compare_and_swap(
39 &self,
40 record: &EffectRecord,
41 expected_revision: Option<u64>,
42 ) -> Result<(), EffectExecutorError>;
43}
44
45#[derive(Clone, Debug, Default)]
47pub struct InMemoryEffectStore {
48 state: Arc<Mutex<StoreState>>,
49}
50
51#[derive(Debug, Default)]
52struct StoreState {
53 records: BTreeMap<EffectId, EffectRecord>,
54 idempotency: BTreeMap<(CapabilityId, String), EffectId>,
55}
56
57impl InMemoryEffectStore {
58 pub fn new() -> Self {
60 Self::default()
61 }
62
63 fn state(&self) -> MutexGuard<'_, StoreState> {
64 self.state
65 .lock()
66 .unwrap_or_else(std::sync::PoisonError::into_inner)
67 }
68}
69
70impl EffectStore for InMemoryEffectStore {
71 fn load(&self, id: EffectId) -> Result<Option<EffectRecord>, EffectExecutorError> {
72 Ok(self.state().records.get(&id).cloned())
73 }
74
75 fn find_by_idempotency(
76 &self,
77 capability_id: CapabilityId,
78 key: &str,
79 ) -> Result<Option<EffectRecord>, EffectExecutorError> {
80 let state = self.state();
81 Ok(state
82 .idempotency
83 .get(&(capability_id, key.into()))
84 .and_then(|id| state.records.get(id))
85 .cloned())
86 }
87
88 fn compare_and_swap(
89 &self,
90 record: &EffectRecord,
91 expected_revision: Option<u64>,
92 ) -> Result<(), EffectExecutorError> {
93 let mut state = self.state();
94 let current = state.records.get(&record.request.effect_id);
95 let valid = match (current, expected_revision) {
96 (None, None) => record.revision == 0,
97 (Some(current), Some(expected)) => {
98 current.revision == expected
99 && expected
100 .checked_add(1)
101 .is_some_and(|next| record.revision == next)
102 }
103 _ => false,
104 };
105 if !valid {
106 return Err(EffectExecutorError::new(
107 EffectExecutorErrorKind::Store,
108 "effect record revision precondition failed",
109 ));
110 }
111
112 if let Some(key) = &record.request.idempotency_key {
113 let index = (record.request.capability_id, key.clone());
114 if let Some(existing) = state.idempotency.get(&index)
115 && *existing != record.request.effect_id
116 {
117 return Err(EffectExecutorError::new(
118 EffectExecutorErrorKind::IdempotencyConflict,
119 "idempotency key already belongs to another effect",
120 ));
121 }
122 state.idempotency.insert(index, record.request.effect_id);
123 }
124 state
125 .records
126 .insert(record.request.effect_id, record.clone());
127 Ok(())
128 }
129}