eidetica/store/mod.rs
1use crate::HeightStrategy;
2use crate::crdt::{CRDT, Doc};
3use crate::{Result, Transaction};
4use async_trait::async_trait;
5use std::marker::PhantomData;
6use std::sync::Arc;
7
8use crate::backend::RecordMutations;
9
10/// Converts canonical Entry deltas to and from a Store's cached record format.
11pub trait RecordProjection<D: CRDT>: Send + Sync {
12 fn descriptor(&self) -> ProjectionDescriptor;
13 fn project_delta(&self, delta: &D, out: &mut RecordMutations) -> Result<()>;
14 fn encode_entry_delta(&self, mutations: &RecordMutations) -> Result<D>;
15
16 /// Converts a caller-facing key into its persisted record key.
17 fn normalize_record_key(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
18 Ok(Some(key.to_vec()))
19 }
20
21 /// Whether two staged keys conflict.
22 ///
23 /// Transactions retain only the newest staged value for each conflicting
24 /// pair. The default preserves exact-key projections.
25 fn staged_keys_conflict(&self, left: &[u8], right: &[u8]) -> bool {
26 left == right
27 }
28
29 /// Whether a staged key makes a cached key stale.
30 fn staged_key_shadows_cached(&self, staged_key: &[u8], cached_key: &[u8]) -> bool {
31 self.staged_keys_conflict(staged_key, cached_key)
32 }
33
34 /// Whether a staged key is a descendant of a caller-facing key.
35 fn staged_key_descends_from(&self, staged_key: &[u8], key: &[u8]) -> bool {
36 staged_key
37 .strip_prefix(key)
38 .is_some_and(|suffix| suffix.starts_with(b"."))
39 }
40}
41
42struct DescribedProjection<D: CRDT + 'static> {
43 descriptor: ProjectionDescriptor,
44 inner: Arc<dyn RecordProjection<D>>,
45}
46
47impl<D: CRDT + 'static> RecordProjection<D> for DescribedProjection<D> {
48 fn descriptor(&self) -> ProjectionDescriptor {
49 self.descriptor.clone()
50 }
51
52 fn project_delta(&self, delta: &D, out: &mut RecordMutations) -> Result<()> {
53 self.inner.project_delta(delta, out)
54 }
55
56 fn encode_entry_delta(&self, mutations: &RecordMutations) -> Result<D> {
57 self.inner.encode_entry_delta(mutations)
58 }
59
60 fn normalize_record_key(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
61 self.inner.normalize_record_key(key)
62 }
63
64 fn staged_keys_conflict(&self, left: &[u8], right: &[u8]) -> bool {
65 self.inner.staged_keys_conflict(left, right)
66 }
67
68 fn staged_key_shadows_cached(&self, staged_key: &[u8], cached_key: &[u8]) -> bool {
69 self.inner.staged_key_shadows_cached(staged_key, cached_key)
70 }
71
72 fn staged_key_descends_from(&self, staged_key: &[u8], key: &[u8]) -> bool {
73 self.inner.staged_key_descends_from(staged_key, key)
74 }
75}
76
77pub mod state;
78pub use crate::backend::ProjectionDescriptor;
79pub use state::OPAQUE_STATE_KEY;
80
81/// Store-owned representation of cached current state.
82///
83/// [`StoreStateModel::Opaque`] caches the complete state in one record.
84/// [`StoreStateModel::Records`] stores individually addressable records derived
85/// from canonical Store deltas.
86pub enum StoreStateModel<D: CRDT + 'static> {
87 /// Safe default: one opaque serialized whole-state record.
88 Opaque {
89 /// Record-format identity; see [`ProjectionDescriptor`].
90 descriptor: ProjectionDescriptor,
91 data: PhantomData<D>,
92 },
93 /// A Store-defined cached record format derived from canonical Store deltas.
94 Records(Arc<dyn RecordProjection<D>>),
95}
96
97impl<D: CRDT + 'static> StoreStateModel<D> {
98 pub fn opaque(name: impl Into<String>, version: u32) -> Self {
99 Self::Opaque {
100 descriptor: ProjectionDescriptor {
101 name: name.into(),
102 version,
103 },
104 data: PhantomData,
105 }
106 }
107
108 pub fn descriptor(&self) -> ProjectionDescriptor {
109 match self {
110 Self::Opaque { descriptor, .. } => descriptor.clone(),
111 Self::Records(projection) => projection.descriptor(),
112 }
113 }
114
115 pub(crate) fn with_descriptor(self, descriptor: ProjectionDescriptor) -> Self {
116 match self {
117 Self::Opaque { .. } => Self::Opaque {
118 descriptor,
119 data: PhantomData,
120 },
121 Self::Records(inner) => {
122 Self::Records(Arc::new(DescribedProjection { descriptor, inner }))
123 }
124 }
125 }
126}
127
128mod errors;
129pub use errors::StoreError;
130
131mod docstore;
132pub use docstore::{DocStore, DocStoreInit};
133
134mod value_editor;
135pub use value_editor::ValueEditor;
136
137pub(crate) mod table;
138pub use table::{Table, TableCursor, TablePage};
139
140mod settings_store;
141pub use settings_store::SettingsStore;
142
143mod registry;
144pub use registry::Registered;
145pub use registry::Registry;
146pub use registry::RegistryEntry;
147pub use registry::SubtreeSettings;
148
149mod password_store;
150pub use password_store::{
151 DEFAULT_ARGON2_M_COST, DEFAULT_ARGON2_P_COST, DEFAULT_ARGON2_T_COST, EncryptedFragment,
152 EncryptionInfo, PasswordStore, PasswordStoreConfig,
153};
154
155#[cfg(feature = "y-crdt")]
156mod ydoc;
157#[cfg(feature = "y-crdt")]
158pub use ydoc::{YDoc, YrsBinary};
159
160/// A trait representing a named, CRDT-based data structure within a `Database`.
161///
162/// `Store` implementations define how data within a specific named partition of a `Database`
163/// is structured, accessed, and modified. They work in conjunction with a `Transaction`
164/// to stage changes before committing them as a single `Entry`.
165///
166/// Users typically interact with `Store` implementations obtained either via:
167/// 1. `Database::get_store_viewer`: For read-only access to the current merged state.
168/// 2. `Transaction::get_store`: For staging modifications within a transaction.
169///
170/// Store types must also implement [`Registered`] to provide their type identifier.
171#[async_trait]
172pub trait Store: Sized + Registered + Send + Sync {
173 /// The CRDT data type used for local (staged) data in this store.
174 ///
175 /// This is the type stored within each individual Entry.
176 type Data: CRDT + 'static;
177
178 /// Representation of this store's cached current state.
179 ///
180 /// By default, the complete state is cached in one opaque record. A Store can
181 /// instead return [`StoreStateModel::Records`] to define addressable records.
182 fn state_model() -> StoreStateModel<Self::Data> {
183 StoreStateModel::opaque("eidetica/opaque", 0)
184 }
185
186 /// Creates a new `Store` handle associated with a specific transaction.
187 ///
188 /// This constructor is typically called internally by `Transaction::get_store` or
189 /// `Database::get_store_viewer`. The resulting `Store` instance provides methods
190 /// to interact with the data of the specified `subtree_name`, potentially staging
191 /// changes within the provided `txn`.
192 ///
193 /// # Arguments
194 /// * `txn` - The `Transaction` this `Store` instance will read from and potentially write to.
195 /// * `subtree_name` - The name identifying this specific data partition within the `Database`.
196 async fn load(txn: &Transaction, subtree_name: String) -> Result<Self>;
197
198 /// Returns the name of this subtree.
199 fn name(&self) -> &str;
200
201 /// Returns a reference to the transaction this Store is associated with.
202 ///
203 /// This is used by the default implementations of `register()`, `get_config()`,
204 /// and `set_config()` to access the index store.
205 fn transaction(&self) -> &Transaction;
206
207 /// Returns the default configuration for this Store type as a [`Doc`].
208 ///
209 /// This configuration is stored in the `_index` subtree when a new subtree is
210 /// first created. The Store implementation owns the format and interpretation
211 /// of this configuration data.
212 ///
213 /// The default implementation returns an empty `Doc`. Store implementations
214 /// that require specific configuration should override this method.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// # use eidetica::{Store, store::DocStore};
220 /// let config = DocStore::default_config();
221 /// assert!(config.is_empty());
222 /// ```
223 fn default_config() -> Doc {
224 Doc::new()
225 }
226
227 /// Initializes a new subtree and registers it in the `_index`.
228 ///
229 /// This method is called by `Transaction::get_store()` when accessing a subtree
230 /// that doesn't yet exist in the `_index`. It creates the Store and registers
231 /// its type and default configuration in the index.
232 ///
233 /// The default implementation:
234 /// 1. Creates the Store using `Self::load()`
235 /// 2. Registers it in `_index` with `Self::type_id()` and `Self::default_config()`
236 ///
237 /// Store implementations can override this to customize initialization behavior.
238 ///
239 /// # Arguments
240 /// * `txn` - The `Transaction` this `Store` instance will operate within.
241 /// * `subtree_name` - The name identifying this specific data partition.
242 ///
243 /// # Returns
244 /// A `Result<Self>` containing the initialized Store.
245 async fn register(txn: &Transaction, subtree_name: String) -> Result<Self> {
246 let store = Self::load(txn, subtree_name).await?;
247 store.set_config(Self::default_config()).await?;
248 Ok(store)
249 }
250
251 /// Opens this Store on a transaction, registering the subtree if it does not
252 /// yet exist.
253 ///
254 /// This is the consumer-facing entry point for attaching a Store to a
255 /// transaction. The default implementation delegates to
256 /// `Transaction::get_store`, which checks `_index` and dispatches to either
257 /// [`Self::load`] (for an existing subtree) or [`Self::register`] (for a new
258 /// one). Stores with construction needs that do not fit the load/register
259 /// split — cross-subtree merge, external state, conditional initialization —
260 /// may override this method directly.
261 ///
262 /// # Arguments
263 /// * `txn` - The transaction this Store will operate within.
264 /// * `name` - The subtree name identifying this data partition.
265 ///
266 /// # Returns
267 /// A `Result<Self>` containing the opened Store.
268 async fn open(txn: &Transaction, name: impl Into<String> + Send) -> Result<Self> {
269 txn.get_store::<Self>(name).await
270 }
271
272 /// Gets the current configuration for this Store from the `_index` subtree.
273 ///
274 /// # Returns
275 /// A `Result<Doc>` containing the configuration document.
276 ///
277 /// # Errors
278 /// Returns an error if the subtree is not registered in `_index`.
279 async fn get_config(&self) -> Result<Doc> {
280 let index = self.transaction().get_index().await?;
281 let info = index.get_entry(self.name()).await?;
282 Ok(info.config)
283 }
284
285 /// Sets the configuration for this Store in the `_index` subtree.
286 ///
287 /// This method updates the `_index` with the Store's type ID and the provided
288 /// configuration. It's called automatically by `register()` and can be used to
289 /// update configuration during a transaction.
290 ///
291 /// # Arguments
292 /// * `config` - The configuration document to store.
293 ///
294 /// # Returns
295 /// A `Result<()>` indicating success or failure.
296 async fn set_config(&self, config: Doc) -> Result<()> {
297 let index = self.transaction().get_index().await?;
298 index
299 .set_entry(self.name(), Self::type_id(), config)
300 .await?;
301 Ok(())
302 }
303
304 /// Gets the height strategy for this Store from the `_index` subtree.
305 ///
306 /// Returns `None` if no strategy is set (meaning the subtree inherits
307 /// from the database-level height strategy).
308 ///
309 /// # Returns
310 /// A `Result<Option<HeightStrategy>>` containing the strategy if set.
311 ///
312 /// # Errors
313 /// Returns an error if the subtree is not registered in `_index`.
314 async fn get_height_strategy(&self) -> Result<Option<HeightStrategy>> {
315 let index = self.transaction().get_index().await?;
316 let settings = index.get_subtree_settings(self.name()).await?;
317 Ok(settings.height_strategy)
318 }
319
320 /// Sets the height strategy for this Store in the `_index` subtree.
321 ///
322 /// Pass `None` to inherit from the database-level strategy,
323 /// or `Some(strategy)` for independent height calculation.
324 ///
325 /// # Arguments
326 /// * `strategy` - The height strategy to use, or None for inheritance.
327 ///
328 /// # Returns
329 /// A `Result<()>` indicating success or failure.
330 ///
331 /// # Errors
332 /// Returns an error if the subtree is not registered in `_index`.
333 async fn set_height_strategy(&self, strategy: Option<HeightStrategy>) -> Result<()> {
334 let index = self.transaction().get_index().await?;
335 let mut settings = index.get_subtree_settings(self.name()).await?;
336 settings.height_strategy = strategy;
337 index.set_subtree_settings(self.name(), settings).await
338 }
339
340 /// Returns the local (staged) data for this store from the current transaction.
341 ///
342 /// This is a convenience method that retrieves data staged in the transaction
343 /// for this store's subtree. Returns `Ok(None)` if no data has been staged.
344 fn local_data(&self) -> Result<Option<Self::Data>> {
345 self.transaction().get_local_data::<Self::Data>(self.name())
346 }
347}