Skip to main content

appcore_sync_sqlite/
config.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: config.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/26 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/26 00:00:00 by dnettoRaw
8//      ###########      S: 2.0.0
9// =============================================================================
10
11//! Defines bounded config contracts and behavior for this crate.
12
13use crate::{SqliteSyncError, SqliteSyncResult};
14use std::fmt;
15use std::path::{Path, PathBuf};
16
17const MIB: u64 = 1024 * 1024;
18
19/// Bounded configuration for one `SQLite` sync database.
20#[derive(Clone, PartialEq, Eq)]
21pub struct SqliteSyncConfig {
22    pub(crate) path: PathBuf,
23    pub(crate) max_database_bytes: u64,
24    pub(crate) max_outbox_entries: usize,
25    pub(crate) max_checkpoints: usize,
26    pub(crate) max_outbox_record_bytes: usize,
27    pub(crate) max_read_records: usize,
28    pub(crate) max_read_bytes: usize,
29    pub(crate) max_tombstones: usize,
30    pub(crate) max_connections: usize,
31    pub(crate) busy_timeout_ms: u64,
32    pub(crate) backup_pages_per_step: i32,
33}
34
35impl SqliteSyncConfig {
36    /// Creates a configuration with conservative production bounds.
37    pub fn new(path: impl Into<PathBuf>) -> Self {
38        Self {
39            path: path.into(),
40            max_database_bytes: 512 * MIB,
41            max_outbox_entries: 10_000,
42            max_checkpoints: 10_000,
43            max_outbox_record_bytes: 48 * MIB as usize,
44            max_read_records: 4_096,
45            max_read_bytes: 16 * MIB as usize,
46            max_tombstones: 100_000,
47            max_connections: 8,
48            busy_timeout_ms: 5_000,
49            backup_pages_per_step: 128,
50        }
51    }
52
53    /// Selects the maximum database size in bytes.
54    pub fn with_max_database_bytes(mut self, value: u64) -> Self {
55        self.max_database_bytes = value;
56        self
57    }
58
59    /// Selects the maximum number of pending outbox entries.
60    pub fn with_max_outbox_entries(mut self, value: usize) -> Self {
61        self.max_outbox_entries = value;
62        self
63    }
64
65    /// Selects the maximum number of peer checkpoints.
66    pub fn with_max_checkpoints(mut self, value: usize) -> Self {
67        self.max_checkpoints = value;
68        self
69    }
70
71    /// Selects the maximum encoded size of one outbox entry.
72    pub fn with_max_outbox_record_bytes(mut self, value: usize) -> Self {
73        self.max_outbox_record_bytes = value;
74        self
75    }
76
77    /// Selects the maximum record count returned by one log read.
78    pub fn with_max_read_records(mut self, value: usize) -> Self {
79        self.max_read_records = value;
80        self
81    }
82
83    /// Selects the maximum payload bytes returned by one log read.
84    pub fn with_max_read_bytes(mut self, value: usize) -> Self {
85        self.max_read_bytes = value;
86        self
87    }
88
89    /// Selects the maximum retained tombstone count.
90    pub fn with_max_tombstones(mut self, value: usize) -> Self {
91        self.max_tombstones = value;
92        self
93    }
94
95    /// Selects the maximum number of simultaneously open `SQLite` connections.
96    pub fn with_max_connections(mut self, value: usize) -> Self {
97        self.max_connections = value;
98        self
99    }
100
101    /// Selects the `SQLite` writer-admission timeout in milliseconds.
102    pub fn with_busy_timeout_ms(mut self, value: u64) -> Self {
103        self.busy_timeout_ms = value;
104        self
105    }
106
107    /// Selects the number of pages copied by each online-backup step.
108    pub fn with_backup_pages_per_step(mut self, value: i32) -> Self {
109        self.backup_pages_per_step = value;
110        self
111    }
112
113    /// Returns the configured database path.
114    pub fn path(&self) -> &Path {
115        &self.path
116    }
117
118    pub(crate) fn validate(&self) -> SqliteSyncResult<()> {
119        if self.path.as_os_str().is_empty() {
120            return Err(SqliteSyncError::InvalidConfiguration("empty path"));
121        }
122        if !(8 * MIB..=8 * 1024 * MIB).contains(&self.max_database_bytes) {
123            return Err(SqliteSyncError::InvalidConfiguration(
124                "database byte bound is outside 8 MiB..=8 GiB",
125            ));
126        }
127        if !(1..=100_000).contains(&self.max_outbox_entries) {
128            return Err(SqliteSyncError::InvalidConfiguration(
129                "outbox entry bound is outside 1..=100000",
130            ));
131        }
132        if !(1..=100_000).contains(&self.max_checkpoints) {
133            return Err(SqliteSyncError::InvalidConfiguration(
134                "checkpoint bound is outside 1..=100000",
135            ));
136        }
137        if !(MIB as usize..=48 * MIB as usize).contains(&self.max_outbox_record_bytes) {
138            return Err(SqliteSyncError::InvalidConfiguration(
139                "outbox record bound is outside 1 MiB..=48 MiB",
140            ));
141        }
142        if !(1..=10_000).contains(&self.max_read_records) {
143            return Err(SqliteSyncError::InvalidConfiguration(
144                "read record bound is outside 1..=10000",
145            ));
146        }
147        if !(MIB as usize..=64 * MIB as usize).contains(&self.max_read_bytes) {
148            return Err(SqliteSyncError::InvalidConfiguration(
149                "read byte bound is outside 1 MiB..=64 MiB",
150            ));
151        }
152        if !(1..=1_000_000).contains(&self.max_tombstones) {
153            return Err(SqliteSyncError::InvalidConfiguration(
154                "tombstone bound is outside 1..=1000000",
155            ));
156        }
157        if !(1..=32).contains(&self.max_connections) {
158            return Err(SqliteSyncError::InvalidConfiguration(
159                "connection bound is outside 1..=32",
160            ));
161        }
162        if !(1..=60_000).contains(&self.busy_timeout_ms) {
163            return Err(SqliteSyncError::InvalidConfiguration(
164                "busy timeout is outside 1..=60000 ms",
165            ));
166        }
167        if !(1..=4_096).contains(&self.backup_pages_per_step) {
168            return Err(SqliteSyncError::InvalidConfiguration(
169                "backup step is outside 1..=4096 pages",
170            ));
171        }
172        Ok(())
173    }
174}
175
176impl fmt::Debug for SqliteSyncConfig {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        formatter
179            .debug_struct("SqliteSyncConfig")
180            .field("path_configured", &!self.path.as_os_str().is_empty())
181            .field("max_database_bytes", &self.max_database_bytes)
182            .field("max_outbox_entries", &self.max_outbox_entries)
183            .field("max_checkpoints", &self.max_checkpoints)
184            .field("max_outbox_record_bytes", &self.max_outbox_record_bytes)
185            .field("max_read_records", &self.max_read_records)
186            .field("max_read_bytes", &self.max_read_bytes)
187            .field("max_tombstones", &self.max_tombstones)
188            .field("max_connections", &self.max_connections)
189            .field("busy_timeout_ms", &self.busy_timeout_ms)
190            .field("backup_pages_per_step", &self.backup_pages_per_step)
191            .finish()
192    }
193}