Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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}