Skip to main content

nodedb_lite/nodedb/core/open/
constructors.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `NodeDbLite` constructors: `open`, `open_with_config`, `open_with_budget`,
4//! and the shared `open_inner` orchestration.
5//!
6//! Every constructor funnels through `open_inner` with a complete
7//! [`LiteConfig`](crate::config::LiteConfig), and `open_inner` is the single
8//! place that consumes it. That is deliberate: a constructor that destructured
9//! its own subset of the config could silently drop a field, which is how
10//! `auto_flush_ms` and `auto_compact_ms` came to be documented as behavior
11//! nothing implemented.
12//!
13//! The constructors return `Arc<Self>` because two of those fields describe
14//! background tasks. The tasks hold a `Weak` back-reference and are spawned
15//! here from the caller's configuration, so the durability and space bounds the
16//! config states hold on the library surface and not only through the FFI and
17//! WASM bindings.
18
19use std::num::NonZeroUsize;
20use std::sync::{Arc, Mutex};
21
22use nodedb_types::error::{NodeDbError, NodeDbResult};
23
24use crate::config::LiteConfig;
25use crate::engine::columnar::ColumnarEngine;
26use crate::engine::fts::FtsState;
27use crate::engine::htap::HtapBridge;
28use crate::engine::sparse_vector::SparseVectorState;
29use crate::engine::strict::StrictEngine;
30use crate::engine::vector::{RestoredVectorState, VectorState};
31use crate::nodedb::lock_ext::LockExt;
32use crate::storage::engine::StorageEngine;
33
34use crate::nodedb::core::kv_local::KvLocalState;
35use crate::nodedb::core::types::NodeDbLite;
36
37impl<S: StorageEngine> NodeDbLite<S> {
38    /// Open or create a Lite database backed by the given storage engine.
39    ///
40    /// Configuration is resolved from environment variables via
41    /// [`LiteConfig::from_env()`], falling back to defaults when variables are
42    /// absent or malformed. The default auto-flush interval is one second.
43    /// The timer schedules flushes without a hard completion deadline.
44    ///
45    /// The instance's Loro peer id is not a parameter: it is minted on first
46    /// open and persisted with the data it authors. A caller-supplied id is
47    /// the same constant in every install of an application, which hands two
48    /// live replicas one producer identity and has the CRDT merge discard one
49    /// of them; and an id that lives only in the caller's argument cannot be
50    /// rotated durably when Origin refuses it.
51    pub async fn open(storage: S) -> NodeDbResult<Arc<Self>> {
52        Self::open_with_config(storage, LiteConfig::from_env()).await
53    }
54
55    /// Open with an explicit [`LiteConfig`].
56    ///
57    /// This is the primary constructor for callers that need control over
58    /// memory budgets or the background maintenance intervals. The config is
59    /// validated before any storage work happens, so an incoherent budget is
60    /// rejected rather than silently over-allocating.
61    pub async fn open_with_config(storage: S, config: LiteConfig) -> NodeDbResult<Arc<Self>> {
62        Self::open_inner(storage, config).await
63    }
64
65    /// Open with a custom memory budget, taking every other setting from
66    /// [`LiteConfig::default()`] — including the default auto-flush interval.
67    ///
68    /// Prefer [`open_with_config`](Self::open_with_config) for new callers.
69    pub async fn open_with_budget(storage: S, memory_budget: usize) -> NodeDbResult<Arc<Self>> {
70        let config = LiteConfig {
71            memory_budget,
72            ..LiteConfig::default()
73        };
74        Self::open_with_config(storage, config).await
75    }
76
77    async fn open_inner(storage: S, config: LiteConfig) -> NodeDbResult<Arc<Self>> {
78        config.validate()?;
79
80        let governor = Arc::new(
81            nodedb_mem::MemoryGovernor::new(config.to_governor_config())
82                .map_err(NodeDbError::config)?,
83        );
84        #[cfg(not(target_arch = "wasm32"))]
85        let sync_enabled = config.sync_enabled;
86        let kv_cache_capacity = NonZeroUsize::new(config.kv_cache_capacity)
87            .ok_or_else(|| NodeDbError::config("kv_cache_capacity must be greater than 0"))?;
88        let kv_local = Arc::new(KvLocalState::new(kv_cache_capacity));
89
90        // Only the outbound sync queues (compiled out on wasm32) consume the cap.
91        #[cfg(not(target_arch = "wasm32"))]
92        let outbound_queue_cap = config.outbound_queue_cap;
93
94        let storage = Arc::new(storage);
95
96        // ── Restore Lite identity + CRDT state (snapshots, bitemporal
97        // backfill, pending deltas, partial-flush safety, legacy CSR cleanup) ──
98        let (crdt, lite_identity, replayed_rows) =
99            Self::restore_identity_and_crdt(&storage, config.corruption_policy).await?;
100
101        // ── Restore FTS indices ──
102        let (fts_manager, fts_checkpoint_complete) =
103            Self::restore_fts_indices(&storage, &governor).await?;
104
105        // ── Restore sparse-vector inverted indices ──
106        let (sparse_manager, sparse_checkpoint_present) =
107            Self::restore_sparse_indices(&storage).await;
108
109        // ── Restore per-collection CSR indices ──
110        let graph_memory = nodedb_mem::ScopedMemory::new(
111            Arc::clone(&governor),
112            nodedb_types::DatabaseId::DEFAULT,
113            nodedb_types::TenantId::new(0),
114            nodedb_mem::EngineId::Graph,
115        );
116        let csr = Self::restore_csr_indices(&storage, &graph_memory).await?;
117
118        // ── Restore HNSW indices and id_map ──
119        let (hnsw_map, hnsw_id_map) = Self::restore_hnsw_indices(&storage).await?;
120
121        // ── Restore spatial indices ──
122        let spatial_memory = nodedb_mem::ScopedMemory::new(
123            Arc::clone(&governor),
124            nodedb_types::DatabaseId::DEFAULT,
125            nodedb_types::TenantId::new(0),
126            nodedb_mem::EngineId::Spatial,
127        );
128        let spatial = Arc::new(Mutex::new(
129            Self::restore_spatial_indices(&storage, &spatial_memory).await,
130        ));
131
132        // ── Restore strict document engine ──
133        let strict = StrictEngine::restore(Arc::clone(&storage))
134            .await
135            .map_err(NodeDbError::storage)?;
136
137        // ── Restore columnar engine ──
138        let columnar_memory = nodedb_mem::ScopedMemory::new(
139            Arc::clone(&governor),
140            nodedb_types::DatabaseId::DEFAULT,
141            nodedb_types::TenantId::new(0),
142            nodedb_mem::EngineId::Columnar,
143        );
144        #[cfg(not(target_arch = "wasm32"))]
145        let mut columnar = ColumnarEngine::restore(Arc::clone(&storage), columnar_memory.clone())
146            .await
147            .map_err(NodeDbError::storage)?;
148        #[cfg(target_arch = "wasm32")]
149        let columnar = ColumnarEngine::restore(Arc::clone(&storage), columnar_memory.clone())
150            .await
151            .map_err(NodeDbError::storage)?;
152
153        // Wire per-engine sync outbound queues when sync is enabled (native only).
154        #[cfg(not(target_arch = "wasm32"))]
155        let outbound_queues =
156            Self::build_outbound_queues(&storage, sync_enabled, outbound_queue_cap, &mut columnar)
157                .await?;
158        #[cfg(not(target_arch = "wasm32"))]
159        let columnar_outbound = outbound_queues.columnar_outbound;
160        #[cfg(not(target_arch = "wasm32"))]
161        let vector_outbound = outbound_queues.vector_outbound;
162        #[cfg(not(target_arch = "wasm32"))]
163        let fts_outbound_init = outbound_queues.fts_outbound;
164        #[cfg(not(target_arch = "wasm32"))]
165        let spatial_outbound_init = outbound_queues.spatial_outbound;
166        #[cfg(not(target_arch = "wasm32"))]
167        let timeseries_outbound_init = outbound_queues.timeseries_outbound;
168        #[cfg(not(target_arch = "wasm32"))]
169        let kv_outbound = outbound_queues.kv_outbound;
170
171        let crdt = Arc::new(Mutex::new(crdt));
172        let strict = Arc::new(strict);
173        let columnar = Arc::new(columnar);
174        let htap = Arc::new(HtapBridge::new());
175        let timeseries = Arc::new(Mutex::new(
176            crate::engine::timeseries::engine::TimeseriesEngine::new(),
177        ));
178        let vector_memory = nodedb_mem::ScopedMemory::new(
179            Arc::clone(&governor),
180            nodedb_types::DatabaseId::DEFAULT,
181            nodedb_types::TenantId::new(0),
182            nodedb_mem::EngineId::Vector,
183        );
184        let vector_state = Arc::new(VectorState::from_restored(RestoredVectorState {
185            storage: Arc::clone(&storage),
186            search_ef: 128,
187            indices: hnsw_map,
188            id_map: hnsw_id_map,
189            memory: vector_memory,
190        }));
191        let fts_state = Arc::new(FtsState::from_restored(fts_manager));
192        let sparse_state = Arc::new(SparseVectorState::from_restored(sparse_manager));
193        let array_engine = crate::engine::array::ArrayEngineState::open(&storage)
194            .await
195            .map_err(NodeDbError::storage)?;
196        let array_state = Arc::new(tokio::sync::Mutex::new(array_engine));
197
198        let csr_arc = Arc::new(Mutex::new(csr));
199        let query_engine =
200            crate::query::LiteQueryEngine::new(crate::query::LiteQueryEngineParams {
201                crdt: Arc::clone(&crdt),
202                strict: Arc::clone(&strict),
203                columnar: Arc::clone(&columnar),
204                htap: Arc::clone(&htap),
205                storage: Arc::clone(&storage),
206                timeseries: Arc::clone(&timeseries),
207                vector_state: Arc::clone(&vector_state),
208                array_state: Arc::clone(&array_state),
209                fts_state: Arc::clone(&fts_state),
210                sparse_state: Arc::clone(&sparse_state),
211                spatial: Arc::clone(&spatial),
212                csr: Arc::clone(&csr_arc),
213                governor: Arc::clone(&governor),
214                kv_local: Arc::clone(&kv_local),
215            });
216
217        // Wire FTS and spatial outbound queues into the query engine so that
218        // SQL-path writes (SpatialOp::Insert, FtsIndexOp) also enqueue for sync.
219        // The queues exist only on native targets, so only native rebinds the
220        // engine as mutable.
221        #[cfg(not(target_arch = "wasm32"))]
222        let query_engine = {
223            let mut query_engine = query_engine;
224            if let Some(ref q) = fts_outbound_init {
225                query_engine.set_fts_outbound(Arc::clone(q));
226            }
227            if let Some(ref q) = spatial_outbound_init {
228                query_engine.set_spatial_outbound(Arc::clone(q));
229            }
230            if let Some(ref q) = kv_outbound {
231                query_engine.set_kv_outbound(Arc::clone(q));
232            }
233            query_engine
234        };
235
236        // ── Array CRDT sync state (send path, receive path, stream sequence
237        // frontier) — non-wasm only ──
238        #[cfg(not(target_arch = "wasm32"))]
239        let array_sync = Self::build_array_sync_state(&storage, &array_state).await?;
240        #[cfg(not(target_arch = "wasm32"))]
241        let array_schemas = array_sync.array_schemas;
242        #[cfg(not(target_arch = "wasm32"))]
243        let array_outbound = array_sync.array_outbound;
244        #[cfg(not(target_arch = "wasm32"))]
245        let array_inbound = array_sync.array_inbound;
246        #[cfg(not(target_arch = "wasm32"))]
247        let stream_seq = array_sync.stream_seq;
248
249        let db = Self {
250            storage,
251            vector_state,
252            csr: csr_arc,
253            crdt,
254            governor,
255            vector_mem_token: Mutex::new(None),
256            graph_mem_token: Mutex::new(None),
257            crdt_mem_token: Mutex::new(None),
258            query_engine,
259            fts_state,
260            sparse_state,
261            spatial,
262            strict,
263            columnar,
264            htap,
265            timeseries,
266            array_state,
267            #[cfg(not(target_arch = "wasm32"))]
268            array_schemas,
269            #[cfg(not(target_arch = "wasm32"))]
270            array_outbound,
271            #[cfg(not(target_arch = "wasm32"))]
272            array_inbound,
273            #[cfg(not(target_arch = "wasm32"))]
274            stream_seq,
275            #[cfg(not(target_arch = "wasm32"))]
276            columnar_outbound,
277            #[cfg(not(target_arch = "wasm32"))]
278            vector_outbound,
279            #[cfg(not(target_arch = "wasm32"))]
280            fts_outbound: fts_outbound_init,
281            #[cfg(not(target_arch = "wasm32"))]
282            spatial_outbound: spatial_outbound_init,
283            #[cfg(not(target_arch = "wasm32"))]
284            timeseries_outbound: timeseries_outbound_init,
285            #[cfg(not(target_arch = "wasm32"))]
286            kv_outbound,
287            identity: Mutex::new(lite_identity),
288            identity_change: tokio::sync::Mutex::new(()),
289            flush_lock: tokio::sync::Mutex::new(()),
290            kv_local,
291            sync_gate: std::sync::RwLock::new(None),
292            tasks: crate::tasks::TaskRegistry::default(),
293        };
294
295        // Remove index entries of the pre-catalog layout, load the secondary
296        // indexes, then rebuild those whose stored entries may not match their
297        // rows (see `index::legacy` and `index::rebuild`).
298        {
299            let mut known: std::collections::BTreeSet<String> = db
300                .crdt
301                .lock_or_recover()
302                .collection_names()
303                .into_iter()
304                .collect();
305            known.extend(db.strict.collection_names());
306            known.extend(db.columnar.collection_names());
307            known.extend(
308                crate::nodedb::collection::ddl::load_persisted_collection_metas(&*db.storage)
309                    .await?
310                    .into_keys(),
311            );
312            crate::index::legacy::clear_legacy_entries(&*db.storage, &known)
313                .await
314                .map_err(NodeDbError::storage)?;
315            let indexes = &db.query_engine.indexes;
316            let loaded = indexes
317                .load(&*db.storage)
318                .await
319                .map_err(NodeDbError::storage)?;
320            crate::index::rebuild::rebuild_at_open(
321                indexes,
322                &*db.storage,
323                &db.crdt,
324                &db.strict,
325                loaded.rebuild_all,
326            )
327            .await
328            .map_err(NodeDbError::storage)?;
329        }
330
331        // Ordinary CRDT postings can reuse compatible checkpoints. Durable strict
332        // and valid-time sources replace their postings on every open.
333        {
334            let fts_empty = db.fts_state.manager.lock_or_recover().is_empty();
335            if fts_empty || !fts_checkpoint_complete || !sparse_checkpoint_present {
336                db.rebuild_text_indices().await?;
337            }
338            db.rebuild_authoritative_text_indices().await?;
339            for (collection, rows) in &replayed_rows {
340                if db.strict.schema(collection).is_some()
341                    || crate::engine::document::history::ops::is_bitemporal(
342                        db.storage.as_ref(),
343                        collection,
344                    )
345                    .await?
346                {
347                    continue;
348                }
349                db.reindex_documents_local(collection, rows.iter().map(String::as_str))?;
350            }
351            db.fts_state.mark_checkpoint_trusted();
352        }
353
354        // Rebuild spatial indices if restore produced empty trees.
355        // The R-tree checkpoint only stores bounding boxes, not doc IDs.
356        // A full rebuild from CRDT documents ensures doc_to_entry is correct.
357        let spatial_empty = db.spatial.lock_or_recover().is_empty();
358        if spatial_empty {
359            db.rebuild_spatial_indices();
360        }
361
362        // Rewrite any edges left over from the legacy pre-EdgeId batch-insert
363        // key format to the EdgeId-keyed form, once. Guarded by a Meta
364        // marker; a no-op scan after the first successful run. Failing this
365        // fails the open, the same way the other restore steps above do.
366        db.migrate_legacy_edge_keys()
367            .await
368            .map_err(NodeDbError::storage)?;
369
370        // Rebuild CSR graph indices when no checkpoint was written before the
371        // previous process exited. Pass 1 reads CRDT edge documents; Pass 2
372        // scans the durable Namespace::Graph KV edge store; Pass 3 reads
373        // Namespace::GraphHistory for bitemporal collections.
374        // The guard is a statement temporary, so it is released before the
375        // rebuild awaits. `db` is not shared yet, so the emptiness check
376        // cannot go stale before the rebuild runs.
377        let csr_empty = db.csr.lock_or_recover().is_empty();
378        if csr_empty {
379            db.rebuild_graph_indices().await;
380        }
381
382        // ── Spawn the background maintenance tasks the config asked for ──
383        // Both hold a `Weak` handle, so they stop when the last `Arc` returned
384        // from here is dropped. A zero interval spawns nothing.
385        let db = Arc::new(db);
386        db.start_auto_flush(config.auto_flush_ms);
387        db.start_auto_compact(config.auto_compact_ms);
388
389        Ok(db)
390    }
391}