storage_kit/state_handle.rs
1//! Private implementation of root state-store owners.
2
3use std::{fmt, future::Future, sync::Arc};
4
5use futures::future::Either;
6
7use crate::state_stores::{self, config::StateStoreConfig};
8
9use crate::state::record::{
10 StateKey, StateListRequest, StateListResponse, StateRecord, StateRecordSnapshot,
11};
12
13/// Transactional control-plane storage with caller-owned associated transactions.
14///
15/// Implement this trait directly; the backend need not implement `Clone`.
16/// A direct [`Self::load`] or [`Self::list`] observes committed data but is not
17/// a fence for a later mutation. Conditional writes must read and validate
18/// their preconditions inside the same associated transaction that commits
19/// the writes. App and Agent own their record schemas, histories and receipts.
20///
21/// # Examples
22///
23/// ```
24/// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
25/// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
26/// use storage_kit::state::record::{StateKey, StateRecord};
27/// # tokio::runtime::Runtime::new()?.block_on(async {
28/// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
29/// let key = StateKey::new("workflow", "run/1")?;
30/// let mut transaction = store.begin_transaction().await?;
31/// transaction.put(StateRecord::new(key.clone(), serde_json::json!({"status": "ready"}))).await?;
32/// transaction.commit().await?;
33/// assert!(store.load(key).await?.is_some());
34/// # Ok::<(), storage_kit::StateStoreError>(())
35/// # })?;
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38///
39/// # Caller-owned store and transaction enums
40///
41/// This complete composition keeps real embedded-Lance semantics. The caller's
42/// `ApplicationStore` selects an application role; its associated `Transaction`
43/// selects the matching child. Neither enum implements Clone. Commit and rollback
44/// consume the transaction and delegate finalization to its actual owner. This is
45/// an illustrative caller composition, not an alternative transaction engine.
46///
47/// ```
48/// use storage_kit::{StateStore, StateStoreBackend, StateTransaction, StateTransactionBackend, StateStoreError};
49/// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
50/// use storage_kit::state::record::{StateKey, StateRecord, StateRecordSnapshot, StateListRequest, StateListResponse};
51///
52/// enum ApplicationStore { Live(StateStore), Scratch(StateStore) }
53/// enum Transaction { Live(StateTransaction), Scratch(StateTransaction) }
54/// impl StateStoreBackend for ApplicationStore {
55/// type Transaction = Transaction;
56/// async fn load(&self, key: StateKey) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
57/// match self { Self::Live(store) => store.load(key).await, Self::Scratch(store) => store.load(key).await }
58/// }
59/// async fn list(&self, request: StateListRequest) -> Result<StateListResponse, StateStoreError> {
60/// match self { Self::Live(store) => store.list(request).await, Self::Scratch(store) => store.list(request).await }
61/// }
62/// async fn begin_transaction(&self) -> Result<Transaction, StateStoreError> {
63/// match self {
64/// Self::Live(store) => Ok(Transaction::Live(store.begin_transaction().await?)),
65/// Self::Scratch(store) => Ok(Transaction::Scratch(store.begin_transaction().await?)),
66/// }
67/// }
68/// }
69/// impl StateTransactionBackend for Transaction {
70/// async fn load(&mut self, key: StateKey) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
71/// match self { Self::Live(tx) => tx.load(key).await, Self::Scratch(tx) => tx.load(key).await }
72/// }
73/// async fn put(&mut self, record: StateRecord) -> Result<(), StateStoreError> {
74/// match self { Self::Live(tx) => tx.put(record).await, Self::Scratch(tx) => tx.put(record).await }
75/// }
76/// async fn delete(&mut self, key: StateKey) -> Result<(), StateStoreError> {
77/// match self { Self::Live(tx) => tx.delete(key).await, Self::Scratch(tx) => tx.delete(key).await }
78/// }
79/// async fn commit(self) -> Result<(), StateStoreError> {
80/// match self { Self::Live(tx) => tx.commit().await, Self::Scratch(tx) => tx.commit().await }
81/// }
82/// async fn rollback(self) -> Result<(), StateStoreError> {
83/// match self { Self::Live(tx) => tx.rollback().await, Self::Scratch(tx) => tx.rollback().await }
84/// }
85/// }
86/// # tokio::runtime::Runtime::new()?.block_on(async {
87/// let live = ApplicationStore::Live(StateStore::connect(
88/// StateStoreConfig::lance(LanceStateStoreConfig::memory()),
89/// ).await?);
90/// let scratch = ApplicationStore::Scratch(StateStore::connect(
91/// StateStoreConfig::lance(LanceStateStoreConfig::memory()),
92/// ).await?);
93/// let key = StateKey::new("jobs", "one")?;
94/// let mut accepted = live.begin_transaction().await?;
95/// accepted.put(StateRecord::new(key.clone(), serde_json::json!("ready"))).await?;
96/// accepted.commit().await?;
97/// assert_eq!(live.load(key.clone()).await?.unwrap().revision.get(), 1);
98/// let mut discarded = scratch.begin_transaction().await?;
99/// discarded.put(StateRecord::new(key.clone(), serde_json::json!("draft"))).await?;
100/// assert!(discarded.load(key.clone()).await?.is_some());
101/// discarded.rollback().await?;
102/// assert!(scratch.load(key).await?.is_none());
103/// drop((live, scratch));
104/// # Ok::<(), StateStoreError>(())
105/// # })?;
106/// # Ok::<(), Box<dyn std::error::Error>>(())
107/// ```
108pub trait StateStoreBackend: Send + Sync + 'static {
109 /// Unique owned transaction returned by this store.
110 type Transaction: StateTransactionBackend;
111
112 /// Loads the current non-deleted value for a key.
113 ///
114 /// # Errors
115 ///
116 /// Returns the owning [`StateStoreError`] when the read cannot complete,
117 /// including unavailable storage, timeout, or record decoding failure.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
123 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
124 /// use storage_kit::state::record::StateKey;
125 /// # tokio::runtime::Runtime::new()?.block_on(async {
126 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
127 /// let key = StateKey::new("workflow", "run/1")?;
128 /// assert!(store.load(key).await?.is_none());
129 /// # Ok::<(), storage_kit::StateStoreError>(())
130 /// # })?;
131 /// # Ok::<(), Box<dyn std::error::Error>>(())
132 /// ```
133 fn load(
134 &self,
135 key: StateKey,
136 ) -> impl Future<Output = Result<Option<StateRecordSnapshot>, StateStoreError>> + Send + '_;
137
138 /// Lists present records in canonical ascending UTF-8 key order.
139 ///
140 /// Namespace and prefix matching are exact and case-sensitive; wildcard
141 /// characters in a prefix are literal. The cursor is exclusive. A
142 /// continuation is the last returned key only when another matching
143 /// record exists in this page's view. Separate pages are not a snapshot;
144 /// callers must fence/revalidate inventories that can change concurrently.
145 ///
146 /// # Errors
147 ///
148 /// Returns the owning [`StateStoreError`] when the page cannot be read or
149 /// decoded, or the configured operation deadline expires.
150 ///
151 /// # Examples
152 ///
153 /// ```
154 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
155 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
156 /// use storage_kit::state::record::{StateKey, StateRecord, StateListRequest};
157 /// # tokio::runtime::Runtime::new()?.block_on(async {
158 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
159 /// let key = StateKey::new("workflow", "run/1")?;
160 /// let mut transaction = store.begin_transaction().await?;
161 /// transaction.put(StateRecord::new(key, serde_json::json!(42))).await?;
162 /// transaction.commit().await?;
163 /// let page = store.list(StateListRequest::new("workflow")?.with_limit(10)?).await?;
164 /// assert_eq!(page.records.len(), 1);
165 /// assert!(page.next_start_after.is_none());
166 /// # Ok::<(), storage_kit::StateStoreError>(())
167 /// # })?;
168 /// # Ok::<(), Box<dyn std::error::Error>>(())
169 /// ```
170 fn list(
171 &self,
172 request: StateListRequest,
173 ) -> impl Future<Output = Result<StateListResponse, StateStoreError>> + Send + '_;
174
175 /// Opens an owned transaction whose successful outcomes are serializable.
176 ///
177 /// Admission may wait or subsequent operations may report an exact
178 /// [`StateStoreError::TransactionConflict`]. The returned transaction's
179 /// reads, including absent keys, participate in its serializable outcome.
180 /// The native transaction may begin lazily at the first operation.
181 ///
182 /// # Errors
183 ///
184 /// Returns [`StateStoreError`] if admission or native transaction setup fails,
185 /// times out, or encounters a serialization conflict.
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
191 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
192 /// # tokio::runtime::Runtime::new()?.block_on(async {
193 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
194 /// let transaction = store.begin_transaction().await?;
195 /// transaction.rollback().await?;
196 /// # Ok::<(), storage_kit::StateStoreError>(())
197 /// # })?;
198 /// # Ok::<(), Box<dyn std::error::Error>>(())
199 /// ```
200 fn begin_transaction(
201 &self,
202 ) -> impl Future<Output = Result<Self::Transaction, StateStoreError>> + Send + '_;
203}
204
205/// A unique, lifetime-free transaction; commit and rollback consume the owner.
206///
207/// Successful commits atomically publish every staged mutation with a
208/// serializable read/write outcome, including missing-key observations.
209/// Reads see this transaction's preceding writes and tombstones. A failed or
210/// cancelled nonterminal operation invalidates further writes/commit and
211/// requires rollback cleanup. Dropping an uncommitted owner never commits.
212/// An admitted commit retains finalization authority after waiter loss.
213///
214/// The trait deliberately has no list or native snapshot operation. Upper
215/// consumers build history from their own immutable records and fenced heads.
216///
217/// # Examples
218///
219/// ```
220/// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
221/// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
222/// use storage_kit::state::record::{StateKey, StateRecord};
223/// # tokio::runtime::Runtime::new()?.block_on(async {
224/// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
225/// let key = StateKey::new("workflow", "run/1")?;
226/// let mut transaction = store.begin_transaction().await?;
227/// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
228/// assert!(transaction.load(key.clone()).await?.is_some());
229/// transaction.rollback().await?;
230/// assert!(store.load(key).await?.is_none());
231/// # Ok::<(), storage_kit::StateStoreError>(())
232/// # })?;
233/// # Ok::<(), Box<dyn std::error::Error>>(())
234/// ```
235pub trait StateTransactionBackend: Send + 'static {
236 /// Loads through the transaction, including earlier writes and tombstones.
237 ///
238 /// # Errors
239 ///
240 /// Returns [`StateStoreError`] for a conflict, timeout, decoding failure, or
241 /// unavailable transaction. A failed nonterminal operation invalidates the
242 /// transaction; roll it back or drop it instead of attempting commit.
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
248 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
249 /// use storage_kit::state::record::{StateKey, StateRecord};
250 /// # tokio::runtime::Runtime::new()?.block_on(async {
251 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
252 /// let key = StateKey::new("workflow", "run/1")?;
253 /// let mut transaction = store.begin_transaction().await?;
254 /// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
255 /// let observed = transaction.load(key).await?.expect("staged record is visible");
256 /// assert_eq!(observed.record.value.as_ref(), &serde_json::json!(42));
257 /// transaction.rollback().await?;
258 /// # Ok::<(), storage_kit::StateStoreError>(())
259 /// # })?;
260 /// # Ok::<(), Box<dyn std::error::Error>>(())
261 /// ```
262 fn load(
263 &mut self,
264 key: StateKey,
265 ) -> impl Future<Output = Result<Option<StateRecordSnapshot>, StateStoreError>> + Send + '_;
266
267 /// Stages a present record and advances its key revision by exactly one.
268 ///
269 /// The first put or delete starts at revision one. Repeated writes count
270 /// individually, including equal values; a retained tombstone is not a
271 /// fresh key. Rollback discards staged revisions with the staged values.
272 ///
273 /// # Errors
274 ///
275 /// Returns [`StateStoreError`] for a revision boundary, serialization failure,
276 /// conflict, timeout, or unavailable transaction. A failure invalidates the
277 /// transaction, so earlier staged writes cannot subsequently commit.
278 ///
279 /// # Examples
280 ///
281 /// ```
282 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
283 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
284 /// use storage_kit::state::record::{StateKey, StateRecord};
285 /// # tokio::runtime::Runtime::new()?.block_on(async {
286 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
287 /// let key = StateKey::new("workflow", "run/1")?;
288 /// let mut transaction = store.begin_transaction().await?;
289 /// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
290 /// assert_eq!(transaction.load(key).await?.expect("staged record").revision.get(), 1);
291 /// transaction.commit().await?;
292 /// # Ok::<(), storage_kit::StateStoreError>(())
293 /// # })?;
294 /// # Ok::<(), Box<dyn std::error::Error>>(())
295 /// ```
296 fn put(
297 &mut self,
298 record: StateRecord,
299 ) -> impl Future<Output = Result<(), StateStoreError>> + Send + '_;
300
301 /// Stages a tombstone and advances the revision, including for an absent key.
302 ///
303 /// Loads then return `None`. A later put increments the retained revision
304 /// instead of resetting it, preventing delete/recreate ABA equality.
305 ///
306 /// # Errors
307 ///
308 /// Returns [`StateStoreError`] for a revision boundary, conflict, timeout, or
309 /// unavailable transaction. A failure invalidates the transaction.
310 ///
311 /// # Examples
312 ///
313 /// ```
314 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
315 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
316 /// use storage_kit::state::record::{StateKey, StateRecord};
317 /// # tokio::runtime::Runtime::new()?.block_on(async {
318 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
319 /// let key = StateKey::new("workflow", "run/1")?;
320 /// let mut transaction = store.begin_transaction().await?;
321 /// transaction.delete(key.clone()).await?;
322 /// assert!(transaction.load(key.clone()).await?.is_none());
323 /// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
324 /// assert_eq!(transaction.load(key).await?.expect("recreated record").revision.get(), 2);
325 /// transaction.commit().await?;
326 /// # Ok::<(), storage_kit::StateStoreError>(())
327 /// # })?;
328 /// # Ok::<(), Box<dyn std::error::Error>>(())
329 /// ```
330 fn delete(
331 &mut self,
332 key: StateKey,
333 ) -> impl Future<Output = Result<(), StateStoreError>> + Send + '_;
334
335 /// Atomically commits all staged mutations and completes native cleanup.
336 ///
337 /// `Ok(())` proves commit. A conflict proves no mutations committed. A
338 /// timeout, transport/finalization error or lost waiter can leave the
339 /// outcome uncertain; it does not prove rollback. Reconcile an upper
340 /// consumer's durable receipt before retrying potentially committed work.
341 /// Dropping the future after admission cannot revoke commit authority.
342 ///
343 /// # Errors
344 ///
345 /// [`StateStoreError::TransactionConflict`] proves this transaction did not
346 /// publish. Timeout, transport, or finalization errors can leave the outcome
347 /// uncertain; inspect a durable application receipt before retrying. A
348 /// previously failed nonterminal operation cannot publish its staged writes.
349 ///
350 /// # Examples
351 ///
352 /// ```
353 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
354 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
355 /// use storage_kit::state::record::{StateKey, StateRecord};
356 /// # tokio::runtime::Runtime::new()?.block_on(async {
357 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
358 /// let key = StateKey::new("workflow", "run/1")?;
359 /// let mut transaction = store.begin_transaction().await?;
360 /// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
361 /// transaction.commit().await?;
362 /// assert!(store.load(key).await?.is_some());
363 /// # Ok::<(), storage_kit::StateStoreError>(())
364 /// # })?;
365 /// # Ok::<(), Box<dyn std::error::Error>>(())
366 /// ```
367 fn commit(self) -> impl Future<Output = Result<(), StateStoreError>> + Send
368 where
369 Self: Sized;
370
371 /// Rolls back all staged mutations and completes rollback cleanup.
372 ///
373 /// Success leaves every committed key and revision unchanged. A primary
374 /// failure must remain distinguishable from a cleanup failure.
375 ///
376 /// # Errors
377 ///
378 /// Returns [`StateStoreError`] if native rollback or resource cleanup fails.
379 /// Preserve any earlier operation failure as the primary error when reporting
380 /// a separate rollback failure.
381 ///
382 /// # Examples
383 ///
384 /// ```
385 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
386 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
387 /// use storage_kit::state::record::{StateKey, StateRecord};
388 /// # tokio::runtime::Runtime::new()?.block_on(async {
389 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
390 /// let key = StateKey::new("workflow", "run/1")?;
391 /// let mut transaction = store.begin_transaction().await?;
392 /// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
393 /// transaction.rollback().await?;
394 /// assert!(store.load(key).await?.is_none());
395 /// # Ok::<(), storage_kit::StateStoreError>(())
396 /// # })?;
397 /// # Ok::<(), Box<dyn std::error::Error>>(())
398 /// ```
399 fn rollback(self) -> impl Future<Output = Result<(), StateStoreError>> + Send
400 where
401 Self: Sized;
402}
403
404/// Cloneable opaque state-store handle.
405///
406/// Clones share the same store. Bring [`StateStoreBackend`] into scope for
407/// reads, listing, and transaction creation; transaction behavior is provided
408/// by [`StateTransactionBackend`]. This handle contains built-ins only.
409///
410/// # Examples
411///
412/// ```
413/// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
414/// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
415/// use storage_kit::state::record::{StateKey, StateRecord};
416/// # tokio::runtime::Runtime::new()?.block_on(async {
417/// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
418/// let shared = store.clone();
419/// let key = StateKey::new("workflow", "run/1")?;
420/// let mut transaction = shared.begin_transaction().await?;
421/// transaction.put(StateRecord::new(key.clone(), serde_json::json!(42))).await?;
422/// transaction.commit().await?;
423/// assert!(store.load(key).await?.is_some());
424/// # Ok::<(), storage_kit::StateStoreError>(())
425/// # })?;
426/// # Ok::<(), Box<dyn std::error::Error>>(())
427/// ```
428#[derive(Clone)]
429pub struct StateStore {
430 inner: Arc<StateStoreInner>,
431}
432
433enum StateStoreInner {
434 Lance(state_stores::lance::LanceStateStore),
435 #[cfg(feature = "redb")]
436 Redb(state_stores::redb::RedbStateStore),
437 #[cfg(feature = "postgres")]
438 Postgres(state_stores::postgres::PostgresStateStore),
439 #[cfg(feature = "sqlite")]
440 Sqlite(state_stores::sqlite::SqliteStateStore),
441}
442
443impl StateStore {
444 /// Opens a built-in state store.
445 ///
446 /// Configuration is validated before opening the selected backend. Lance
447 /// is always available. Redb, PostgreSQL, and SQLite/Turso require `redb`,
448 /// `postgres`, and `sqlite`, respectively. A disabled implementation returns
449 /// [`StateStoreError::InvalidConfig`] before opening native resources.
450 ///
451 /// # Errors
452 ///
453 /// Returns [`StateStoreError::InvalidConfig`] for invalid configuration, or
454 /// the owning I/O, connection, setup, or timeout error when opening fails.
455 ///
456 /// # Examples
457 ///
458 /// ```
459 /// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
460 /// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
461 /// use storage_kit::state::record::StateKey;
462 /// # tokio::runtime::Runtime::new()?.block_on(async {
463 /// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
464 /// assert!(store.load(StateKey::new("workflow", "run/1")?).await?.is_none());
465 /// # Ok::<(), storage_kit::StateStoreError>(())
466 /// # })?;
467 /// # Ok::<(), Box<dyn std::error::Error>>(())
468 /// ```
469 pub async fn connect(config: StateStoreConfig) -> Result<Self, StateStoreError> {
470 config.validate()?;
471 match config {
472 StateStoreConfig::Lance(config) => {
473 state_stores::lance::LanceStateStore::connect(config)
474 .await
475 .map(Self::from_lance)
476 }
477 StateStoreConfig::Redb(config) => {
478 if cfg!(feature = "redb") {
479 #[cfg(feature = "redb")]
480 return state_stores::redb::RedbStateStore::connect(config)
481 .await
482 .map(Self::from_redb);
483 }
484 drop(config);
485 Err(StateStoreError::InvalidConfig(
486 "Redb requires the `redb` feature".into(),
487 ))
488 }
489 StateStoreConfig::Postgres(config) => {
490 if cfg!(feature = "postgres") {
491 #[cfg(feature = "postgres")]
492 return state_stores::postgres::PostgresStateStore::connect(config)
493 .await
494 .map(Self::from_postgres);
495 }
496 drop(config);
497 Err(StateStoreError::InvalidConfig(
498 "PostgreSQL requires the `postgres` feature".into(),
499 ))
500 }
501 #[cfg(feature = "sqlite")]
502 StateStoreConfig::Sqlite(config) => {
503 state_stores::sqlite::SqliteStateStore::connect(config)
504 .await
505 .map(Self::from_sqlite)
506 }
507 }
508 }
509
510 #[cfg(feature = "redb")]
511 fn from_redb(store: state_stores::redb::RedbStateStore) -> Self {
512 Self {
513 inner: Arc::new(StateStoreInner::Redb(store)),
514 }
515 }
516
517 fn from_lance(store: state_stores::lance::LanceStateStore) -> Self {
518 Self {
519 inner: Arc::new(StateStoreInner::Lance(store)),
520 }
521 }
522
523 #[cfg(feature = "postgres")]
524 fn from_postgres(store: state_stores::postgres::PostgresStateStore) -> Self {
525 Self {
526 inner: Arc::new(StateStoreInner::Postgres(store)),
527 }
528 }
529
530 #[cfg(feature = "sqlite")]
531 fn from_sqlite(store: state_stores::sqlite::SqliteStateStore) -> Self {
532 Self {
533 inner: Arc::new(StateStoreInner::Sqlite(store)),
534 }
535 }
536}
537
538impl fmt::Debug for StateStore {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 f.write_str("StateStore { .. }")
541 }
542}
543
544impl StateStoreBackend for StateStore {
545 type Transaction = StateTransaction;
546
547 async fn load(&self, key: StateKey) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
548 self.inner.load(key).await
549 }
550
551 async fn list(&self, request: StateListRequest) -> Result<StateListResponse, StateStoreError> {
552 self.inner.list(request).await
553 }
554
555 async fn begin_transaction(&self) -> Result<Self::Transaction, StateStoreError> {
556 self.inner.begin_transaction().await
557 }
558}
559
560impl StateStoreBackend for StateStoreInner {
561 type Transaction = StateTransaction;
562
563 async fn load(&self, key: StateKey) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
564 match self {
565 Self::Lance(store) => store.load(key).await,
566 #[cfg(feature = "redb")]
567 Self::Redb(store) => store.load(key).await,
568 #[cfg(feature = "postgres")]
569 Self::Postgres(store) => store.load(key).await,
570 #[cfg(feature = "sqlite")]
571 Self::Sqlite(store) => store.load(key).await,
572 }
573 }
574
575 async fn list(&self, request: StateListRequest) -> Result<StateListResponse, StateStoreError> {
576 match self {
577 Self::Lance(store) => store.list(request).await,
578 #[cfg(feature = "redb")]
579 Self::Redb(store) => store.list(request).await,
580 #[cfg(feature = "postgres")]
581 Self::Postgres(store) => store.list(request).await,
582 #[cfg(feature = "sqlite")]
583 Self::Sqlite(store) => store.list(request).await,
584 }
585 }
586
587 async fn begin_transaction(&self) -> Result<Self::Transaction, StateStoreError> {
588 match self {
589 Self::Lance(store) => store
590 .begin_transaction()
591 .await
592 .map(StateTransaction::from_lance),
593 #[cfg(feature = "redb")]
594 Self::Redb(store) => store
595 .begin_transaction()
596 .await
597 .map(StateTransaction::from_redb),
598 #[cfg(feature = "postgres")]
599 Self::Postgres(store) => store
600 .begin_transaction()
601 .await
602 .map(StateTransaction::from_postgres),
603 #[cfg(feature = "sqlite")]
604 Self::Sqlite(store) => store
605 .begin_transaction()
606 .await
607 .map(StateTransaction::from_sqlite),
608 }
609 }
610}
611
612/// Unique, lifetime-free opaque state transaction.
613///
614/// Obtain this handle from [`StateStoreBackend::begin_transaction`]. Import
615/// [`StateTransactionBackend`] to read, stage writes, and consume it through
616/// commit or rollback. It is not cloneable; dropping an uncommitted handle
617/// settles rollback cleanup, while an admitted commit retains finalization
618/// authority even if its reply is lost.
619///
620/// See [`StateStoreBackend::begin_transaction`] for construction and rollback.
621pub struct StateTransaction {
622 inner: StateTransactionInner,
623}
624
625enum StateTransactionInner {
626 Lance(state_stores::lance::LanceStateTransaction),
627 #[cfg(feature = "redb")]
628 Redb(state_stores::redb::RedbStateTransaction),
629 #[cfg(feature = "postgres")]
630 Postgres(state_stores::postgres::PostgresStateTransaction),
631 #[cfg(feature = "sqlite")]
632 Sqlite(state_stores::sqlite::SqliteStateTransaction),
633}
634
635impl StateTransaction {
636 fn from_lance(transaction: state_stores::lance::LanceStateTransaction) -> Self {
637 Self {
638 inner: StateTransactionInner::Lance(transaction),
639 }
640 }
641
642 #[cfg(feature = "redb")]
643 fn from_redb(transaction: state_stores::redb::RedbStateTransaction) -> Self {
644 Self {
645 inner: StateTransactionInner::Redb(transaction),
646 }
647 }
648
649 #[cfg(feature = "postgres")]
650 fn from_postgres(transaction: state_stores::postgres::PostgresStateTransaction) -> Self {
651 Self {
652 inner: StateTransactionInner::Postgres(transaction),
653 }
654 }
655
656 #[cfg(feature = "sqlite")]
657 fn from_sqlite(transaction: state_stores::sqlite::SqliteStateTransaction) -> Self {
658 Self {
659 inner: StateTransactionInner::Sqlite(transaction),
660 }
661 }
662}
663
664impl fmt::Debug for StateTransaction {
665 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666 f.write_str("StateTransaction { .. }")
667 }
668}
669
670impl StateTransactionBackend for StateTransaction {
671 async fn load(
672 &mut self,
673 key: StateKey,
674 ) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
675 self.inner.load(key).await
676 }
677
678 fn put(
679 &mut self,
680 record: StateRecord,
681 ) -> impl Future<Output = Result<(), StateStoreError>> + Send {
682 self.inner.put(record)
683 }
684
685 async fn delete(&mut self, key: StateKey) -> Result<(), StateStoreError> {
686 self.inner.delete(key).await
687 }
688
689 async fn commit(self) -> Result<(), StateStoreError> {
690 self.inner.commit().await
691 }
692
693 async fn rollback(self) -> Result<(), StateStoreError> {
694 self.inner.rollback().await
695 }
696}
697
698impl StateTransactionBackend for StateTransactionInner {
699 async fn load(
700 &mut self,
701 key: StateKey,
702 ) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
703 match self {
704 Self::Lance(transaction) => transaction.load(key).await,
705 #[cfg(feature = "redb")]
706 Self::Redb(transaction) => transaction.load(key).await,
707 #[cfg(feature = "postgres")]
708 Self::Postgres(transaction) => transaction.load(key).await,
709 #[cfg(feature = "sqlite")]
710 Self::Sqlite(transaction) => transaction.load(key).await,
711 }
712 }
713
714 fn put(
715 &mut self,
716 record: StateRecord,
717 ) -> impl Future<Output = Result<(), StateStoreError>> + Send {
718 // Construct Lance's future before returning so it owns deep JSON even
719 // if the caller drops this operation without polling it.
720 let prepared: Either<_, (&mut Self, StateRecord)> = match self {
721 Self::Lance(transaction) => Either::Left(transaction.put(record)),
722 #[cfg(feature = "redb")]
723 transaction @ Self::Redb(_) => Either::Right((transaction, record)),
724 #[cfg(feature = "postgres")]
725 transaction @ Self::Postgres(_) => Either::Right((transaction, record)),
726 #[cfg(feature = "sqlite")]
727 transaction @ Self::Sqlite(_) => Either::Right((transaction, record)),
728 };
729 async move {
730 match prepared {
731 Either::Left(operation) => operation.await,
732 Either::Right((transaction, record)) => match transaction {
733 Self::Lance(transaction) => transaction.put(record).await,
734 #[cfg(feature = "redb")]
735 Self::Redb(transaction) => transaction.put(record).await,
736 #[cfg(feature = "postgres")]
737 Self::Postgres(transaction) => transaction.put(record).await,
738 #[cfg(feature = "sqlite")]
739 Self::Sqlite(transaction) => transaction.put(record).await,
740 },
741 }
742 }
743 }
744
745 async fn delete(&mut self, key: StateKey) -> Result<(), StateStoreError> {
746 match self {
747 Self::Lance(transaction) => transaction.delete(key).await,
748 #[cfg(feature = "redb")]
749 Self::Redb(transaction) => transaction.delete(key).await,
750 #[cfg(feature = "postgres")]
751 Self::Postgres(transaction) => transaction.delete(key).await,
752 #[cfg(feature = "sqlite")]
753 Self::Sqlite(transaction) => transaction.delete(key).await,
754 }
755 }
756
757 async fn commit(self) -> Result<(), StateStoreError> {
758 match self {
759 Self::Lance(transaction) => transaction.commit().await,
760 #[cfg(feature = "redb")]
761 Self::Redb(transaction) => transaction.commit().await,
762 #[cfg(feature = "postgres")]
763 Self::Postgres(transaction) => transaction.commit().await,
764 #[cfg(feature = "sqlite")]
765 Self::Sqlite(transaction) => transaction.commit().await,
766 }
767 }
768
769 async fn rollback(self) -> Result<(), StateStoreError> {
770 match self {
771 Self::Lance(transaction) => transaction.rollback().await,
772 #[cfg(feature = "redb")]
773 Self::Redb(transaction) => transaction.rollback().await,
774 #[cfg(feature = "postgres")]
775 Self::Postgres(transaction) => transaction.rollback().await,
776 #[cfg(feature = "sqlite")]
777 Self::Sqlite(transaction) => transaction.rollback().await,
778 }
779 }
780}
781
782/// Portable state-store error.
783///
784/// A [`Self::TransactionConflict`] establishes non-publication of that
785/// transaction. A timeout, unavailable connection, or backend finalization
786/// error does not by itself establish rollback. Reconcile durable application
787/// state before retrying a potentially committed mutation. Error messages
788/// provided by caller backends must not contain credentials or native payloads.
789///
790/// # Examples
791///
792/// ```
793/// use storage_kit::{StateStoreError};
794/// let error = StateStoreError::transaction_conflict("commit", "concurrent write");
795/// assert!(matches!(error, StateStoreError::TransactionConflict { .. }));
796/// ```
797#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
798pub enum StateStoreError {
799 /// Invalid configuration.
800 #[error("invalid state store configuration: {0}")]
801 InvalidConfig(String),
802 /// Invalid caller request.
803 #[error("invalid state store request: {0}")]
804 InvalidRequest(String),
805 /// Backend unavailable.
806 #[error("state store is unavailable during `{operation}`: {message}")]
807 Unavailable {
808 /// Portable operation.
809 operation: String,
810 /// Secret-safe message.
811 message: String,
812 },
813 /// Backend operation failure.
814 #[error("state store operation `{operation}` failed: {message}")]
815 BackendOperation {
816 /// Portable operation.
817 operation: String,
818 /// Secret-safe message.
819 message: String,
820 },
821 /// Exact transaction conflict.
822 #[error("state store transaction `{operation}` conflicted: {message}")]
823 TransactionConflict {
824 /// Portable operation.
825 operation: String,
826 /// Secret-safe message.
827 message: String,
828 },
829 /// Configured timeout elapsed.
830 #[error("state store operation `{operation}` timed out after {timeout_ms} ms")]
831 TimedOut {
832 /// Portable operation.
833 operation: String,
834 /// Timeout in milliseconds.
835 timeout_ms: u64,
836 },
837 /// JSON serialization failed.
838 #[error("state store serialization failed: {0}")]
839 Serialization(String),
840}
841
842impl StateStoreError {
843 /// Creates an invalid-config error.
844 ///
845 /// # Examples
846 ///
847 /// ```
848 /// use storage_kit::{StateStoreError};
849 /// let error = StateStoreError::invalid_config("timeout must be positive");
850 /// assert!(matches!(error, StateStoreError::InvalidConfig(_)));
851 /// ```
852 pub fn invalid_config(message: impl Into<String>) -> Self {
853 Self::InvalidConfig(message.into())
854 }
855
856 /// Creates an invalid-request error.
857 ///
858 /// # Examples
859 ///
860 /// ```
861 /// use storage_kit::{StateStoreError};
862 /// let error = StateStoreError::invalid_request("key is empty");
863 /// assert!(matches!(error, StateStoreError::InvalidRequest(_)));
864 /// ```
865 pub fn invalid_request(message: impl Into<String>) -> Self {
866 Self::InvalidRequest(message.into())
867 }
868
869 /// Creates an unavailable error.
870 ///
871 /// # Examples
872 ///
873 /// ```
874 /// use storage_kit::{StateStoreError};
875 /// let error = StateStoreError::unavailable("load", "connection is closed");
876 /// assert!(matches!(error, StateStoreError::Unavailable { .. }));
877 /// ```
878 pub fn unavailable(operation: impl Into<String>, message: impl Into<String>) -> Self {
879 Self::Unavailable {
880 operation: operation.into(),
881 message: message.into(),
882 }
883 }
884
885 /// Creates a backend-operation error.
886 ///
887 /// # Examples
888 ///
889 /// ```
890 /// use storage_kit::{StateStoreError};
891 /// let error = StateStoreError::backend_operation("load", "record could not be read");
892 /// assert!(matches!(error, StateStoreError::BackendOperation { .. }));
893 /// ```
894 pub fn backend_operation(operation: impl Into<String>, message: impl Into<String>) -> Self {
895 Self::BackendOperation {
896 operation: operation.into(),
897 message: message.into(),
898 }
899 }
900
901 /// Creates an exact transaction-conflict error.
902 ///
903 /// Use this category only when the transaction did not publish its mutations.
904 /// Do not convert every concurrent timeout or transport failure into conflict.
905 ///
906 /// See [`StateStoreError`] for an executable example.
907 pub fn transaction_conflict(operation: impl Into<String>, message: impl Into<String>) -> Self {
908 Self::TransactionConflict {
909 operation: operation.into(),
910 message: message.into(),
911 }
912 }
913
914 /// Creates a timeout error.
915 ///
916 /// This records a deadline failure; it does not establish whether a commit
917 /// admitted before the timeout eventually published.
918 ///
919 /// # Examples
920 ///
921 /// ```
922 /// use storage_kit::{StateStoreError};
923 /// let error = StateStoreError::timed_out("commit", 500);
924 /// assert!(matches!(error, StateStoreError::TimedOut { timeout_ms: 500, .. }));
925 /// ```
926 pub fn timed_out(operation: impl Into<String>, timeout_ms: u64) -> Self {
927 Self::TimedOut {
928 operation: operation.into(),
929 timeout_ms,
930 }
931 }
932
933 /// Creates a serialization error.
934 ///
935 /// # Examples
936 ///
937 /// ```
938 /// use storage_kit::{StateStoreError};
939 /// let error = StateStoreError::serialization("invalid record encoding");
940 /// assert!(matches!(error, StateStoreError::Serialization(_)));
941 /// ```
942 pub fn serialization(message: impl Into<String>) -> Self {
943 Self::Serialization(message.into())
944 }
945}
946
947impl From<serde_json::Error> for StateStoreError {
948 fn from(error: serde_json::Error) -> Self {
949 Self::serialization(error.to_string())
950 }
951}