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}