Skip to main content

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/// # Examples
621///
622/// ```
623/// use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
624/// use storage_kit::state::config::{StateStoreConfig, LanceStateStoreConfig};
625/// # tokio::runtime::Runtime::new()?.block_on(async {
626/// let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
627/// let transaction = store.begin_transaction().await?;
628/// transaction.rollback().await?;
629/// # Ok::<(), storage_kit::StateStoreError>(())
630/// # })?;
631/// # Ok::<(), Box<dyn std::error::Error>>(())
632/// ```
633pub struct StateTransaction {
634    inner: StateTransactionInner,
635}
636
637enum StateTransactionInner {
638    Lance(state_stores::lance::LanceStateTransaction),
639    #[cfg(feature = "redb")]
640    Redb(state_stores::redb::RedbStateTransaction),
641    #[cfg(feature = "postgres")]
642    Postgres(state_stores::postgres::PostgresStateTransaction),
643    #[cfg(feature = "sqlite")]
644    Sqlite(state_stores::sqlite::SqliteStateTransaction),
645}
646
647impl StateTransaction {
648    fn from_lance(transaction: state_stores::lance::LanceStateTransaction) -> Self {
649        Self {
650            inner: StateTransactionInner::Lance(transaction),
651        }
652    }
653
654    #[cfg(feature = "redb")]
655    fn from_redb(transaction: state_stores::redb::RedbStateTransaction) -> Self {
656        Self {
657            inner: StateTransactionInner::Redb(transaction),
658        }
659    }
660
661    #[cfg(feature = "postgres")]
662    fn from_postgres(transaction: state_stores::postgres::PostgresStateTransaction) -> Self {
663        Self {
664            inner: StateTransactionInner::Postgres(transaction),
665        }
666    }
667
668    #[cfg(feature = "sqlite")]
669    fn from_sqlite(transaction: state_stores::sqlite::SqliteStateTransaction) -> Self {
670        Self {
671            inner: StateTransactionInner::Sqlite(transaction),
672        }
673    }
674}
675
676impl fmt::Debug for StateTransaction {
677    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678        f.write_str("StateTransaction { .. }")
679    }
680}
681
682impl StateTransactionBackend for StateTransaction {
683    async fn load(
684        &mut self,
685        key: StateKey,
686    ) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
687        self.inner.load(key).await
688    }
689
690    fn put(
691        &mut self,
692        record: StateRecord,
693    ) -> impl Future<Output = Result<(), StateStoreError>> + Send {
694        self.inner.put(record)
695    }
696
697    async fn delete(&mut self, key: StateKey) -> Result<(), StateStoreError> {
698        self.inner.delete(key).await
699    }
700
701    async fn commit(self) -> Result<(), StateStoreError> {
702        self.inner.commit().await
703    }
704
705    async fn rollback(self) -> Result<(), StateStoreError> {
706        self.inner.rollback().await
707    }
708}
709
710impl StateTransactionBackend for StateTransactionInner {
711    async fn load(
712        &mut self,
713        key: StateKey,
714    ) -> Result<Option<StateRecordSnapshot>, StateStoreError> {
715        match self {
716            Self::Lance(transaction) => transaction.load(key).await,
717            #[cfg(feature = "redb")]
718            Self::Redb(transaction) => transaction.load(key).await,
719            #[cfg(feature = "postgres")]
720            Self::Postgres(transaction) => transaction.load(key).await,
721            #[cfg(feature = "sqlite")]
722            Self::Sqlite(transaction) => transaction.load(key).await,
723        }
724    }
725
726    fn put(
727        &mut self,
728        record: StateRecord,
729    ) -> impl Future<Output = Result<(), StateStoreError>> + Send {
730        // Construct Lance's future before returning so it owns deep JSON even
731        // if the caller drops this operation without polling it.
732        let prepared: Either<_, (&mut Self, StateRecord)> = match self {
733            Self::Lance(transaction) => Either::Left(transaction.put(record)),
734            #[cfg(feature = "redb")]
735            transaction @ Self::Redb(_) => Either::Right((transaction, record)),
736            #[cfg(feature = "postgres")]
737            transaction @ Self::Postgres(_) => Either::Right((transaction, record)),
738            #[cfg(feature = "sqlite")]
739            transaction @ Self::Sqlite(_) => Either::Right((transaction, record)),
740        };
741        async move {
742            match prepared {
743                Either::Left(operation) => operation.await,
744                Either::Right((transaction, record)) => match transaction {
745                    Self::Lance(transaction) => transaction.put(record).await,
746                    #[cfg(feature = "redb")]
747                    Self::Redb(transaction) => transaction.put(record).await,
748                    #[cfg(feature = "postgres")]
749                    Self::Postgres(transaction) => transaction.put(record).await,
750                    #[cfg(feature = "sqlite")]
751                    Self::Sqlite(transaction) => transaction.put(record).await,
752                },
753            }
754        }
755    }
756
757    async fn delete(&mut self, key: StateKey) -> Result<(), StateStoreError> {
758        match self {
759            Self::Lance(transaction) => transaction.delete(key).await,
760            #[cfg(feature = "redb")]
761            Self::Redb(transaction) => transaction.delete(key).await,
762            #[cfg(feature = "postgres")]
763            Self::Postgres(transaction) => transaction.delete(key).await,
764            #[cfg(feature = "sqlite")]
765            Self::Sqlite(transaction) => transaction.delete(key).await,
766        }
767    }
768
769    async fn commit(self) -> Result<(), StateStoreError> {
770        match self {
771            Self::Lance(transaction) => transaction.commit().await,
772            #[cfg(feature = "redb")]
773            Self::Redb(transaction) => transaction.commit().await,
774            #[cfg(feature = "postgres")]
775            Self::Postgres(transaction) => transaction.commit().await,
776            #[cfg(feature = "sqlite")]
777            Self::Sqlite(transaction) => transaction.commit().await,
778        }
779    }
780
781    async fn rollback(self) -> Result<(), StateStoreError> {
782        match self {
783            Self::Lance(transaction) => transaction.rollback().await,
784            #[cfg(feature = "redb")]
785            Self::Redb(transaction) => transaction.rollback().await,
786            #[cfg(feature = "postgres")]
787            Self::Postgres(transaction) => transaction.rollback().await,
788            #[cfg(feature = "sqlite")]
789            Self::Sqlite(transaction) => transaction.rollback().await,
790        }
791    }
792}
793
794/// Portable state-store error.
795///
796/// A [`Self::TransactionConflict`] establishes non-publication of that
797/// transaction. A timeout, unavailable connection, or backend finalization
798/// error does not by itself establish rollback. Reconcile durable application
799/// state before retrying a potentially committed mutation. Error messages
800/// provided by caller backends must not contain credentials or native payloads.
801///
802/// # Examples
803///
804/// ```
805/// use storage_kit::{StateStoreError};
806/// let error = StateStoreError::transaction_conflict("commit", "concurrent write");
807/// assert!(matches!(error, StateStoreError::TransactionConflict { .. }));
808/// ```
809#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
810pub enum StateStoreError {
811    /// Invalid configuration.
812    #[error("invalid state store configuration: {0}")]
813    InvalidConfig(String),
814    /// Invalid caller request.
815    #[error("invalid state store request: {0}")]
816    InvalidRequest(String),
817    /// Backend unavailable.
818    #[error("state store is unavailable during `{operation}`: {message}")]
819    Unavailable {
820        /// Portable operation.
821        operation: String,
822        /// Secret-safe message.
823        message: String,
824    },
825    /// Backend operation failure.
826    #[error("state store operation `{operation}` failed: {message}")]
827    BackendOperation {
828        /// Portable operation.
829        operation: String,
830        /// Secret-safe message.
831        message: String,
832    },
833    /// Exact transaction conflict.
834    #[error("state store transaction `{operation}` conflicted: {message}")]
835    TransactionConflict {
836        /// Portable operation.
837        operation: String,
838        /// Secret-safe message.
839        message: String,
840    },
841    /// Configured timeout elapsed.
842    #[error("state store operation `{operation}` timed out after {timeout_ms} ms")]
843    TimedOut {
844        /// Portable operation.
845        operation: String,
846        /// Timeout in milliseconds.
847        timeout_ms: u64,
848    },
849    /// JSON serialization failed.
850    #[error("state store serialization failed: {0}")]
851    Serialization(String),
852}
853
854impl StateStoreError {
855    /// Creates an invalid-config error.
856    ///
857    /// # Examples
858    ///
859    /// ```
860    /// use storage_kit::{StateStoreError};
861    /// let error = StateStoreError::invalid_config("timeout must be positive");
862    /// assert!(matches!(error, StateStoreError::InvalidConfig(_)));
863    /// ```
864    pub fn invalid_config(message: impl Into<String>) -> Self {
865        Self::InvalidConfig(message.into())
866    }
867
868    /// Creates an invalid-request error.
869    ///
870    /// # Examples
871    ///
872    /// ```
873    /// use storage_kit::{StateStoreError};
874    /// let error = StateStoreError::invalid_request("key is empty");
875    /// assert!(matches!(error, StateStoreError::InvalidRequest(_)));
876    /// ```
877    pub fn invalid_request(message: impl Into<String>) -> Self {
878        Self::InvalidRequest(message.into())
879    }
880
881    /// Creates an unavailable error.
882    ///
883    /// # Examples
884    ///
885    /// ```
886    /// use storage_kit::{StateStoreError};
887    /// let error = StateStoreError::unavailable("load", "connection is closed");
888    /// assert!(matches!(error, StateStoreError::Unavailable { .. }));
889    /// ```
890    pub fn unavailable(operation: impl Into<String>, message: impl Into<String>) -> Self {
891        Self::Unavailable {
892            operation: operation.into(),
893            message: message.into(),
894        }
895    }
896
897    /// Creates a backend-operation error.
898    ///
899    /// # Examples
900    ///
901    /// ```
902    /// use storage_kit::{StateStoreError};
903    /// let error = StateStoreError::backend_operation("load", "record could not be read");
904    /// assert!(matches!(error, StateStoreError::BackendOperation { .. }));
905    /// ```
906    pub fn backend_operation(operation: impl Into<String>, message: impl Into<String>) -> Self {
907        Self::BackendOperation {
908            operation: operation.into(),
909            message: message.into(),
910        }
911    }
912
913    /// Creates an exact transaction-conflict error.
914    ///
915    /// Use this category only when the transaction did not publish its mutations.
916    /// Do not convert every concurrent timeout or transport failure into conflict.
917    ///
918    /// # Examples
919    ///
920    /// ```
921    /// use storage_kit::{StateStoreError};
922    /// let error = StateStoreError::transaction_conflict("commit", "concurrent write");
923    /// assert!(matches!(error, StateStoreError::TransactionConflict { .. }));
924    /// ```
925    pub fn transaction_conflict(operation: impl Into<String>, message: impl Into<String>) -> Self {
926        Self::TransactionConflict {
927            operation: operation.into(),
928            message: message.into(),
929        }
930    }
931
932    /// Creates a timeout error.
933    ///
934    /// This records a deadline failure; it does not establish whether a commit
935    /// admitted before the timeout eventually published.
936    ///
937    /// # Examples
938    ///
939    /// ```
940    /// use storage_kit::{StateStoreError};
941    /// let error = StateStoreError::timed_out("commit", 500);
942    /// assert!(matches!(error, StateStoreError::TimedOut { timeout_ms: 500, .. }));
943    /// ```
944    pub fn timed_out(operation: impl Into<String>, timeout_ms: u64) -> Self {
945        Self::TimedOut {
946            operation: operation.into(),
947            timeout_ms,
948        }
949    }
950
951    /// Creates a serialization error.
952    ///
953    /// # Examples
954    ///
955    /// ```
956    /// use storage_kit::{StateStoreError};
957    /// let error = StateStoreError::serialization("invalid record encoding");
958    /// assert!(matches!(error, StateStoreError::Serialization(_)));
959    /// ```
960    pub fn serialization(message: impl Into<String>) -> Self {
961        Self::Serialization(message.into())
962    }
963}
964
965impl From<serde_json::Error> for StateStoreError {
966    fn from(error: serde_json::Error) -> Self {
967        Self::serialization(error.to_string())
968    }
969}