Skip to main content

zerodds_durability_store/
store.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! The [`DurabilityStore`] trait — the storage abstraction every cold adapter
5//! implements (ADR 0009, layer ②b).
6
7use alloc::vec::Vec;
8use std::time::SystemTime;
9
10use crate::contract::Contract;
11use crate::error::Result;
12use crate::model::{DurabilitySample, Page, Selector, StoreStats};
13
14/// Default page size for the [`DurabilityStore::replay_for_topic`] convenience
15/// drain when a selector does not set its own `limit`.
16pub const DEFAULT_PAGE: usize = 1024;
17
18/// A durable, contract-bounded sample store. Implementations are the cold
19/// adapters (sqlite / file / lakehouse / in-memory). All methods are
20/// `&self` + `Send + Sync`: the daemon shares one store across threads.
21///
22/// Retention is bounded ONLY by the topic's `Contract` (ADR 0009 inv. 1):
23/// implementations enforce it on [`store`](DurabilityStore::store) (drop-oldest
24/// under `KEEP_LAST`, [`StoreError::OutOfResources`](crate::StoreError) under
25/// `KEEP_ALL` caps).
26pub trait DurabilityStore: Send + Sync {
27    /// Registers (or replaces) the retention [`Contract`] for `topic`. The
28    /// daemon calls this when it starts serving a topic. Topics without an
29    /// explicit contract use the store's default ([`Contract::default`]).
30    ///
31    /// # Errors
32    /// Backend/I/O failure.
33    fn set_contract(&self, topic: &str, contract: Contract) -> Result<()>;
34
35    /// Persists `sample` durably, enforcing the topic contract. Returns
36    /// [`StoreError::OutOfResources`](crate::StoreError) when a `KEEP_ALL` cap
37    /// is hit.
38    ///
39    /// # Errors
40    /// Contract cap exceeded, or a backend/I/O failure.
41    fn store(&self, sample: DurabilitySample) -> Result<()>;
42
43    /// Returns one ordered [`Page`] of samples for `topic` matching `selector`.
44    /// Ordering is `(instance_key, sequence)`. Drive pagination via
45    /// [`Page::next`] + [`Selector::after_cursor`].
46    ///
47    /// # Errors
48    /// Backend/I/O failure.
49    fn query(&self, topic: &str, selector: &Selector) -> Result<Page>;
50
51    /// Marks an instance as unregistered at `now`; its samples become eligible
52    /// for purge after `now + cleanup_delay` (applied by [`cleanup`]).
53    ///
54    /// [`cleanup`]: DurabilityStore::cleanup
55    ///
56    /// # Errors
57    /// Backend/I/O failure.
58    fn unregister(&self, topic: &str, instance_key: &[u8; 16], now: SystemTime) -> Result<()>;
59
60    /// Purges instances whose `unregister` time + cleanup-delay is `<= now`.
61    /// Returns the number of purged instances.
62    ///
63    /// # Errors
64    /// Backend/I/O failure.
65    fn cleanup(&self, now: SystemTime) -> Result<usize>;
66
67    /// Aggregate counters for `topic`.
68    ///
69    /// # Errors
70    /// Backend/I/O failure.
71    fn stats(&self, topic: &str) -> Result<StoreStats>;
72
73    /// Convenience: drains the full contracted history for `topic` by paging
74    /// through [`query`](DurabilityStore::query). Prefer paged `query` for
75    /// large contracts; this materializes everything into a `Vec`.
76    ///
77    /// # Errors
78    /// Backend/I/O failure from the underlying `query`.
79    fn replay_for_topic(&self, topic: &str) -> Result<Vec<DurabilitySample>> {
80        let mut out = Vec::new();
81        let mut sel = Selector {
82            limit: Some(DEFAULT_PAGE),
83            ..Selector::default()
84        };
85        loop {
86            let page = self.query(topic, &sel)?;
87            let got = page.samples.len();
88            out.extend(page.samples);
89            match page.next {
90                Some(cursor) if got > 0 => sel = sel.after_cursor(cursor),
91                _ => break,
92            }
93        }
94        Ok(out)
95    }
96}