1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// SPDX-License-Identifier: Apache-2.0
//! The [`LiteConfig`] struct and its defaults.
use serde::{Deserialize, Serialize};
use super::defaults::{
default_argon2_m_cost, default_argon2_p_cost, default_argon2_t_cost, default_array_percent,
default_auto_compact_ms, default_auto_flush_ms, default_columnar_percent,
default_document_percent, default_fts_percent, default_kv_cache_capacity, default_kv_percent,
default_outbound_queue_cap, default_sparse_percent, default_spatial_percent,
default_strict_percent, default_sync_enabled, default_timeseries_percent,
};
use crate::storage::corruption::CorruptionPolicy;
/// Runtime configuration for a NodeDB-Lite instance.
///
/// All percentage fields express a fraction of `memory_budget` allocated to
/// the corresponding engine. The remaining percentage is headroom (untracked).
///
/// # Example
/// ```
/// use nodedb_lite::config::LiteConfig;
///
/// let cfg = LiteConfig {
/// memory_budget: 256 * 1024 * 1024, // 256 MiB
/// ..LiteConfig::default()
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LiteConfig {
/// Total memory budget in bytes. Default: 100 MiB.
pub memory_budget: usize,
/// Percentage of `memory_budget` reserved for HNSW vector index. Default: 40.
pub hnsw_percent: usize,
/// Percentage of `memory_budget` reserved for CSR graph index. Default: 15.
pub csr_percent: usize,
/// Percentage of `memory_budget` reserved for Loro CRDT engine. Default: 15.
pub loro_percent: usize,
/// Percentage of `memory_budget` reserved for query scratch space. Default: 15.
pub query_percent: usize,
/// Percentage of `memory_budget` reserved for the key-value engine. Default: 2.
#[serde(default = "default_kv_percent")]
pub kv_percent: usize,
/// Percentage of `memory_budget` reserved for the schemaless document engine. Default: 2.
#[serde(default = "default_document_percent")]
pub document_percent: usize,
/// Percentage of `memory_budget` reserved for the strict document engine. Default: 2.
#[serde(default = "default_strict_percent")]
pub strict_percent: usize,
/// Percentage of `memory_budget` reserved for the columnar engine. Default: 2.
#[serde(default = "default_columnar_percent")]
pub columnar_percent: usize,
/// Percentage of `memory_budget` reserved for the timeseries engine. Default: 1.
#[serde(default = "default_timeseries_percent")]
pub timeseries_percent: usize,
/// Percentage of `memory_budget` reserved for the spatial engine. Default: 1.
#[serde(default = "default_spatial_percent")]
pub spatial_percent: usize,
/// Percentage of `memory_budget` reserved for the full-text search engine. Default: 1.
#[serde(default = "default_fts_percent")]
pub fts_percent: usize,
/// Percentage of `memory_budget` reserved for the array engine. Default: 1.
#[serde(default = "default_array_percent")]
pub array_percent: usize,
/// Percentage of `memory_budget` reserved for the sparse-vector metadata engine. Default: 1.
#[serde(default = "default_sparse_percent")]
pub sparse_percent: usize,
/// Open the durable outbound sync queues. Default: `true`.
///
/// When `true`, KV, columnar, timeseries, vector, FTS and spatial writes
/// record themselves in their outbound queue, and the push loop sends
/// them to Origin. When `false`, those writes stay local. Documents sync
/// through Loro either way.
#[serde(default = "default_sync_enabled")]
pub sync_enabled: bool,
/// Argon2id memory cost in KiB. Default: 19 MiB (19_456 KiB).
/// Corresponds to the OWASP recommended minimum for interactive login.
#[serde(default = "default_argon2_m_cost")]
pub argon2_m_cost: u32,
/// Argon2id iteration count. Default: 2.
#[serde(default = "default_argon2_t_cost")]
pub argon2_t_cost: u32,
/// Argon2id parallelism lanes. Default: 1.
#[serde(default = "default_argon2_p_cost")]
pub argon2_p_cost: u32,
/// Maximum entries in the in-memory KV read cache. Default: 10_000.
///
/// Each entry holds the raw encoded value (typically ~80 bytes for a
/// 64-byte payload + 8-byte TTL framing), so 10 000 entries ≈ 800 KB.
///
/// A value of 0 is rejected at open time; use 1 as the effective minimum.
#[serde(default = "default_kv_cache_capacity")]
pub kv_cache_capacity: usize,
/// Maximum number of pending entries in each durable outbound queue
/// (columnar and timeseries). Default: 100_000.
///
/// When a queue reaches this cap, write operations return
/// [`LiteError::Backpressure`] until the sync transport drains entries.
/// This bounds RAM usage to the key/pointer overhead regardless of how
/// long the device stays offline; the payloads themselves are on disk.
///
/// Can also be set via the `NODEDB_LITE_OUTBOUND_QUEUE_CAP` environment
/// variable.
#[serde(default = "default_outbound_queue_cap")]
pub outbound_queue_cap: usize,
/// Scheduled interval between background flushes, in milliseconds. Default: 1000 (one second).
///
/// Opening a database starts a task that applies buffered document, CRDT, and KV state and persists derived checkpoints.
/// Scheduling delays and storage errors prevent a hard wall-clock durability bound.
/// The task stops when the last database handle disappears.
///
/// Strict row writes commit before success. Native filesystem pagedb commits survive reopening without an explicit flush.
/// Generic storage backends provide their own commit durability guarantees.
/// Unindexed KV puts and all KV deletes remain buffered. Indexed KV puts and SQL KV writes commit directly.
/// Derived checkpoint maintenance remains separate from strict commits. Later maintenance errors do not roll back committed rows.
///
/// Set to 0 to disable background flushing. Call `flush()` before dropping buffered state.
#[serde(default = "default_auto_flush_ms")]
pub auto_flush_ms: u64,
/// Interval between automatic background compactions, in milliseconds.
/// Default: 0 (disabled).
///
/// When non-zero, opening a database spawns a background task that calls
/// the global `compact()` every `auto_compact_ms` milliseconds, reclaiming
/// dead pages and truncating the backing file to bound on-disk growth.
/// Unlike auto-flush this is **opt-in**: compaction is a heavier operation
/// (it repacks B+ trees and truncates the file, and no-ops while a reader
/// pins the reclaimable range), so it is not imposed on every embedder by
/// default.
///
/// Enable it when writing one commit per entry (where the deferred-free
/// list would otherwise grow unbounded). A much larger interval than
/// `auto_flush_ms` is appropriate — e.g. minutes, not seconds. Set to 0 to
/// leave compaction fully manual via `compact()`.
#[serde(default = "default_auto_compact_ms")]
pub auto_compact_ms: u64,
/// What to do when the store being opened cannot be read.
/// Default: [`CorruptionPolicy::FailClosed`].
///
/// The default refuses to open and leaves the damaged store untouched, so
/// an embedder that has no Origin to re-sync from does not silently start
/// against an empty database. Selecting
/// [`CorruptionPolicy::DiscardStoreAndRecreate`] is a statement that the
/// data can be rebuilt from somewhere else.
#[serde(default)]
pub corruption_policy: CorruptionPolicy,
}
impl Default for LiteConfig {
fn default() -> Self {
Self {
memory_budget: 100 * 1024 * 1024, // 100 MiB
hnsw_percent: 40,
csr_percent: 15,
loro_percent: 15,
query_percent: 15,
kv_percent: default_kv_percent(),
document_percent: default_document_percent(),
strict_percent: default_strict_percent(),
columnar_percent: default_columnar_percent(),
timeseries_percent: default_timeseries_percent(),
spatial_percent: default_spatial_percent(),
fts_percent: default_fts_percent(),
array_percent: default_array_percent(),
sparse_percent: default_sparse_percent(),
sync_enabled: true,
outbound_queue_cap: default_outbound_queue_cap(),
argon2_m_cost: default_argon2_m_cost(),
argon2_t_cost: default_argon2_t_cost(),
argon2_p_cost: default_argon2_p_cost(),
kv_cache_capacity: default_kv_cache_capacity(),
auto_flush_ms: default_auto_flush_ms(),
auto_compact_ms: default_auto_compact_ms(),
corruption_policy: CorruptionPolicy::FailClosed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let cfg = LiteConfig::default();
assert_eq!(cfg.memory_budget, 100 * 1024 * 1024);
assert_eq!(cfg.hnsw_percent, 40);
assert_eq!(cfg.csr_percent, 15);
assert_eq!(cfg.loro_percent, 15);
assert_eq!(cfg.query_percent, 15);
assert_eq!(cfg.kv_percent, 2);
assert_eq!(cfg.document_percent, 2);
assert_eq!(cfg.strict_percent, 2);
assert_eq!(cfg.columnar_percent, 2);
assert_eq!(cfg.timeseries_percent, 1);
assert_eq!(cfg.spatial_percent, 1);
assert_eq!(cfg.fts_percent, 1);
assert_eq!(cfg.array_percent, 1);
assert_eq!(cfg.sparse_percent, 1);
assert_eq!(cfg.argon2_m_cost, 19_456);
assert_eq!(cfg.argon2_t_cost, 2);
assert_eq!(cfg.argon2_p_cost, 1);
assert_eq!(cfg.auto_flush_ms, 1_000);
// Auto-compaction is opt-in: disabled by default.
assert_eq!(cfg.auto_compact_ms, 0);
}
#[test]
fn serde_roundtrip() {
let cfg = LiteConfig::default();
let json = sonic_rs::to_string(&cfg).unwrap();
let parsed: LiteConfig = sonic_rs::from_str(&json).unwrap();
assert_eq!(parsed, cfg);
}
}