nodedb-lite 0.1.0

Embedded edge client for NodeDB — offline vector search, graph traversal, and CRDT sync
Documentation
// 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);
    }
}