storage_kit/state/mod.rs
1//! Revisioned control-plane state and owned transactions.
2//!
3//! Open a supplied [`crate::StateStore`] from [`config::StateStoreConfig`],
4//! import [`crate::StateStoreBackend`] for reads and transaction creation, and
5//! import [`crate::StateTransactionBackend`] to stage and finalize mutations.
6//! [`record`] contains validated identities, write inputs, observations, and
7//! pagination. Transactions remain unique, lifetime-free values and are
8//! consumed by commit or rollback.
9//!
10//! Read conditional-write preconditions inside the committing transaction.
11//! Ordinary reads and separate listing pages do not establish a snapshot.
12//! A lost commit reply may require durable reconciliation before retrying.
13
14//! # Preconditions, revisions and pagination
15//!
16//! This runs on embedded Lance without a service. Conditional writes validate their
17//! precondition inside the same transaction. Rollback consumes the transaction and
18//! discards its staged revision. Do not automatically retry an ambiguous commit.
19//!
20//! ```
21//! use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
22//! use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
23//! use storage_kit::state::record::{StateKey, StateListRequest, StateRecord};
24//! # tokio::runtime::Runtime::new()?.block_on(async {
25//! let store = StateStore::connect(StateStoreConfig::lance(LanceStateStoreConfig::memory())).await?;
26//! let key = StateKey::new("jobs", "a")?;
27//! let mut create = store.begin_transaction().await?;
28//! assert!(create.load(key.clone()).await?.is_none());
29//! create.put(StateRecord::new(key.clone(), serde_json::json!({"status":"ready"}))).await?;
30//! create.put(StateRecord::new(StateKey::new("jobs", "b")?, serde_json::json!({"status":"ready"}))).await?;
31//! create.commit().await?;
32//!
33//! let mut update = store.begin_transaction().await?;
34//! let before = update.load(key.clone()).await?.expect("created job");
35//! assert_eq!(before.revision.get(), 1);
36//! assert_eq!(before.record.value["status"], "ready");
37//! update.put(StateRecord::new(key.clone(), serde_json::json!({"status":"running"}))).await?;
38//! assert_eq!(update.load(key.clone()).await?.unwrap().revision.get(), 2);
39//! update.rollback().await?;
40//! assert_eq!(store.load(key.clone()).await?.unwrap().revision.get(), 1);
41//!
42//! let first = store.list(StateListRequest::new("jobs")?.with_limit(1)?).await?;
43//! assert_eq!(first.records.len(), 1);
44//! let cursor = first.next_start_after.expect("one remaining job");
45//! let second = store.list(StateListRequest::new("jobs")?.with_limit(1)?.start_after(cursor)?).await?;
46//! assert_eq!(second.records.len(), 1);
47//! assert!(second.next_start_after.is_none());
48//! // Separate pages observe committed state independently, not one frozen snapshot.
49//! let mut replace = store.begin_transaction().await?;
50//! replace.delete(key.clone()).await?;
51//! assert!(replace.load(key.clone()).await?.is_none());
52//! replace.put(StateRecord::new(key.clone(), serde_json::json!({"status":"new"}))).await?;
53//! replace.commit().await?;
54//! assert_eq!(store.load(key).await?.unwrap().revision.get(), 3);
55//! drop(store);
56//! # Ok::<(), storage_kit::StateStoreError>(())
57//! # })?;
58//! # Ok::<(), Box<dyn std::error::Error>>(())
59//! ```
60//!
61//! A caller detecting a stale application precondition should roll back instead of
62//! writing. Native serialization conflicts use [`crate::StateStoreError::TransactionConflict`];
63//! a timeout or lost commit reply does not establish that the write was rolled back.
64//!
65//! # Directory-backed reopen
66//!
67//! Memory belongs to one connected owner and its clones. Directory connections
68//! reopen persisted records. This example owns its temporary directory, consumes
69//! all transactions and drops both store handles before removing that directory.
70//! StateStore has no public shutdown operation; final-handle cleanup belongs to
71//! the selected implementation.
72//!
73//! ```
74//! use storage_kit::{StateStore, StateStoreBackend, StateTransactionBackend};
75//! use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
76//! use storage_kit::state::record::{StateKey, StateRecord};
77//! let directory = std::env::temp_dir().join(format!("ai-kit-state-doc-{}", uuid::Uuid::new_v4()));
78//! std::fs::create_dir(&directory)?;
79//! let result = tokio::runtime::Runtime::new()?.block_on(async {
80//! let config = StateStoreConfig::lance(LanceStateStoreConfig::directory(&directory));
81//! let store = StateStore::connect(config.clone()).await?;
82//! let key = StateKey::new("settings", "theme")?;
83//! let mut transaction = store.begin_transaction().await?;
84//! transaction.put(StateRecord::new(key.clone(), serde_json::json!("dark"))).await?;
85//! transaction.commit().await?;
86//! drop(store);
87//! let reopened = StateStore::connect(config).await?;
88//! let observed = reopened.load(key).await?.expect("durable record");
89//! assert_eq!(observed.record.value.as_ref(), &serde_json::json!("dark"));
90//! assert_eq!(observed.revision.get(), 1);
91//! drop(reopened);
92//! Ok::<(), storage_kit::StateStoreError>(())
93//! });
94//! let cleanup = std::fs::remove_dir_all(&directory);
95//! result?; // Preserve the operation error if cleanup also failed.
96//! cleanup?;
97//! # Ok::<(), Box<dyn std::error::Error>>(())
98//! ```
99//!
100//! Custom stores retain their own concrete associated transaction. The complete
101//! [composition example](crate::StateStoreBackend) delegates native commit and
102//! rollback instead of inventing transaction guarantees.
103
104/// Built-in state-store construction data.
105///
106/// Lance is always available. Redb, PostgreSQL, and SQLite/Turso require
107/// explicit `redb`, `postgres`, and `sqlite` features. Constructors and validation perform no connection I/O;
108/// [`crate::StateStore::connect`] opens the selected store.
109pub mod config {
110 pub use crate::credentials::Password;
111 pub use crate::state_stores::config::{
112 LanceStateStoreConfig, LanceStateStoreLocation, PostgresStateStoreConfig,
113 PostgresStateStoreTlsMode, RedbStateStoreConfig, RedbStateStoreLocation, StateStoreConfig,
114 };
115 #[cfg(feature = "sqlite")]
116 pub use crate::state_stores::config::{SqliteStateStoreConfig, SqliteStateStoreLocation};
117}
118
119/// Revisioned records, keys, and pagination.
120pub mod record;
121
122/// The unique owned transaction handle.
123///
124/// [`StateTransaction`](crate::state::transaction::StateTransaction) implements [`crate::StateTransactionBackend`]. Obtain
125/// it from [`crate::StateStoreBackend::begin_transaction`]; it has no public
126/// constructor. Caller-written stores return their own associated transaction
127/// type instead of constructing this supplied handle.
128pub mod transaction {
129 pub use crate::state_handle::StateTransaction;
130}