converge_core/traits/
store.rs1use std::future::Future;
10use std::time::Duration;
11
12use super::error::{CapabilityError, ErrorCategory};
13use crate::context::ContextState;
14
15#[derive(Debug, Clone)]
21pub enum StoreError {
22 Unavailable { message: String },
24 SerializationFailed { message: String },
26 Conflict { event_id: String },
28 InvalidQuery { message: String },
30 AuthFailed { message: String },
32 RateLimited { retry_after: Duration },
34 Timeout {
36 elapsed: Duration,
37 deadline: Duration,
38 },
39 NotFound { message: String },
41 InvariantViolation { message: String },
43 Internal { message: String },
45}
46
47impl std::fmt::Display for StoreError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Unavailable { message } => write!(f, "store unavailable: {message}"),
51 Self::SerializationFailed { message } => write!(f, "serialization failed: {message}"),
52 Self::Conflict { event_id } => write!(f, "already exists: {event_id}"),
53 Self::InvalidQuery { message } => write!(f, "invalid query: {message}"),
54 Self::AuthFailed { message } => write!(f, "store auth failed: {message}"),
55 Self::RateLimited { retry_after } => {
56 write!(f, "rate limited, retry after {retry_after:?}")
57 }
58 Self::Timeout { elapsed, deadline } => {
59 write!(f, "timed out after {elapsed:?} (deadline: {deadline:?})")
60 }
61 Self::NotFound { message } => write!(f, "not found: {message}"),
62 Self::InvariantViolation { message } => write!(f, "invariant violation: {message}"),
63 Self::Internal { message } => write!(f, "internal store error: {message}"),
64 }
65 }
66}
67
68impl std::error::Error for StoreError {}
69
70impl CapabilityError for StoreError {
71 fn category(&self) -> ErrorCategory {
72 match self {
73 Self::Unavailable { .. } => ErrorCategory::Unavailable,
74 Self::SerializationFailed { .. } | Self::InvalidQuery { .. } => {
75 ErrorCategory::InvalidInput
76 }
77 Self::Conflict { .. } => ErrorCategory::Conflict,
78 Self::AuthFailed { .. } => ErrorCategory::Auth,
79 Self::RateLimited { .. } => ErrorCategory::RateLimit,
80 Self::Timeout { .. } => ErrorCategory::Timeout,
81 Self::NotFound { .. } => ErrorCategory::NotFound,
82 Self::InvariantViolation { .. } => ErrorCategory::InvariantViolation,
83 Self::Internal { .. } => ErrorCategory::Internal,
84 }
85 }
86
87 fn is_transient(&self) -> bool {
88 matches!(
89 self,
90 Self::Unavailable { .. } | Self::RateLimited { .. } | Self::Timeout { .. }
91 )
92 }
93
94 fn is_retryable(&self) -> bool {
95 matches!(
96 self,
97 Self::Unavailable { .. }
98 | Self::RateLimited { .. }
99 | Self::Timeout { .. }
100 | Self::Conflict { .. }
101 )
102 }
103
104 fn retry_after(&self) -> Option<Duration> {
105 match self {
106 Self::RateLimited { retry_after } => Some(*retry_after),
107 _ => None,
108 }
109 }
110}
111
112pub trait ContextStore: Send + Sync {
122 type LoadFut<'a>: Future<Output = Result<Option<ContextState>, StoreError>> + Send + 'a
124 where
125 Self: 'a;
126
127 type SaveFut<'a>: Future<Output = Result<(), StoreError>> + Send + 'a
129 where
130 Self: 'a;
131
132 fn load_context<'a>(&'a self, scope_id: &'a str) -> Self::LoadFut<'a>;
134
135 fn save_context<'a>(
137 &'a self,
138 scope_id: &'a str,
139 context: &'a ContextState,
140 ) -> Self::SaveFut<'a>;
141}
142
143pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
149
150pub trait DynContextStore: Send + Sync {
152 fn load_context<'a>(
154 &'a self,
155 scope_id: &'a str,
156 ) -> BoxFuture<'a, Result<Option<ContextState>, StoreError>>;
157
158 fn save_context<'a>(
160 &'a self,
161 scope_id: &'a str,
162 context: &'a ContextState,
163 ) -> BoxFuture<'a, Result<(), StoreError>>;
164}
165
166impl<T: ContextStore> DynContextStore for T {
167 fn load_context<'a>(
168 &'a self,
169 scope_id: &'a str,
170 ) -> BoxFuture<'a, Result<Option<ContextState>, StoreError>> {
171 Box::pin(ContextStore::load_context(self, scope_id))
172 }
173
174 fn save_context<'a>(
175 &'a self,
176 scope_id: &'a str,
177 context: &'a ContextState,
178 ) -> BoxFuture<'a, Result<(), StoreError>> {
179 Box::pin(ContextStore::save_context(self, scope_id, context))
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn store_error_display() {
189 let err = StoreError::Conflict {
190 event_id: "evt-123".to_string(),
191 };
192 assert!(err.to_string().contains("evt-123"));
193 }
194
195 #[test]
196 fn store_error_category_classification() {
197 assert_eq!(
198 StoreError::Unavailable {
199 message: "test".to_string()
200 }
201 .category(),
202 ErrorCategory::Unavailable
203 );
204 assert_eq!(
205 StoreError::Conflict {
206 event_id: "test".to_string()
207 }
208 .category(),
209 ErrorCategory::Conflict
210 );
211 }
212
213 #[test]
214 fn store_error_transient_classification() {
215 assert!(
216 StoreError::Unavailable {
217 message: "test".to_string()
218 }
219 .is_transient()
220 );
221 assert!(
222 !StoreError::Conflict {
223 event_id: "test".to_string()
224 }
225 .is_transient()
226 );
227 }
228
229 #[test]
230 fn store_error_retryable_classification() {
231 assert!(
232 StoreError::Unavailable {
233 message: "test".to_string()
234 }
235 .is_retryable()
236 );
237 assert!(
238 !StoreError::AuthFailed {
239 message: "test".to_string()
240 }
241 .is_retryable()
242 );
243 }
244
245 #[test]
246 fn store_error_retry_after() {
247 let err = StoreError::RateLimited {
248 retry_after: Duration::from_secs(60),
249 };
250 assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
251 }
252}