use std::sync::Arc;
use std::time::Duration;
use nodedb_client::NodeDb;
use nodedb_lite::{Encryption, LiteConfig, NodeDbLite, PagedbStorageDefault};
use nodedb_types::document::Document;
use nodedb_types::value::Value;
#[tokio::test]
async fn auto_flush_persists_without_explicit_flush() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("auto_flush_persist.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let config = LiteConfig {
auto_flush_ms: 200,
..LiteConfig::default()
};
let db = Arc::new(
NodeDbLite::open_with_config(storage, config)
.await
.expect("open db"),
);
db.start_auto_flush(200);
db.kv_put("col", "key", b"auto_flushed")
.await
.expect("kv_put");
tokio::time::sleep(Duration::from_millis(450)).await;
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert_eq!(
got.as_deref(),
Some(b"auto_flushed".as_slice()),
"key must survive reopen when auto-flush fired before drop"
);
}
}
#[tokio::test]
async fn disabled_auto_flush_does_not_persist() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("auto_flush_disabled.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let config = LiteConfig {
auto_flush_ms: 0,
..LiteConfig::default()
};
let db = Arc::new(
NodeDbLite::open_with_config(storage, config)
.await
.expect("open db"),
);
db.start_auto_flush(0);
db.kv_put("col", "key", b"unflushed").await.expect("kv_put");
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert!(
got.is_none(),
"key must NOT survive reopen when auto-flush is disabled and flush() was not called; \
got: {got:?}"
);
}
}
#[tokio::test]
async fn open_with_config_honors_auto_flush_ms() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config_auto_flush.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let config = LiteConfig {
auto_flush_ms: 200,
..LiteConfig::default()
};
let db = NodeDbLite::open_with_config(storage, config)
.await
.expect("open db");
db.kv_put("col", "key", b"config_flushed")
.await
.expect("kv_put");
tokio::time::sleep(Duration::from_millis(450)).await;
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert_eq!(
got.as_deref(),
Some(b"config_flushed".as_slice()),
"a write must survive reopen once auto_flush_ms has elapsed, with the interval \
supplied through LiteConfig alone"
);
}
}
#[tokio::test]
async fn open_with_config_bounds_crdt_state_durability() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config_auto_flush_crdt.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let config = LiteConfig {
auto_flush_ms: 200,
..LiteConfig::default()
};
let db = NodeDbLite::open_with_config(storage, config)
.await
.expect("open db");
db.execute_sql("CREATE COLLECTION bt_notes WITH (bitemporal=true)", &[])
.await
.expect("create bitemporal collection");
for i in 0u32..8 {
let mut doc = Document::new(format!("note{i}"));
doc.set("body", Value::String(format!("entry {i}")));
db.document_put("bt_notes", doc)
.await
.expect("document_put");
}
tokio::time::sleep(Duration::from_millis(450)).await;
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let fetched = db
.document_get("bt_notes", "note3")
.await
.expect("document_get after reopen");
assert!(
fetched.is_some(),
"bitemporal rows must survive reopen (they commit per write)"
);
let dump = db.diagnostic_dump().await;
assert!(
dump.storage_counts.loro_state > 0,
"CRDT state must be persisted within auto_flush_ms as well — rows reading back \
correctly while loro_state is empty is the silent-divergence failure: the replica \
can no longer sync those rows to an Origin; storage_counts: {:?}",
dump.storage_counts
);
}
}
#[tokio::test]
async fn open_honors_default_auto_flush_ms() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("default_auto_flush.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let db = NodeDbLite::open(storage).await.expect("open db");
db.kv_put("col", "key", b"default_flushed")
.await
.expect("kv_put");
tokio::time::sleep(Duration::from_millis(1_500)).await;
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert_eq!(
got.as_deref(),
Some(b"default_flushed".as_slice()),
"the default auto_flush_ms must bound durability for a plain open()"
);
}
}
#[tokio::test]
async fn open_with_budget_honors_default_auto_flush_ms() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("budget_auto_flush.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let db = NodeDbLite::open_with_budget(storage, 64 * 1024 * 1024)
.await
.expect("open db");
db.kv_put("col", "key", b"budget_flushed")
.await
.expect("kv_put");
tokio::time::sleep(Duration::from_millis(1_500)).await;
db.shutdown().await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert_eq!(
got.as_deref(),
Some(b"budget_flushed".as_slice()),
"open_with_budget must honor the default auto_flush_ms it inherits from LiteConfig"
);
}
}
#[tokio::test]
async fn open_at_path_with_config_honors_auto_flush_ms() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("at_path_auto_flush.pagedb");
{
let config = LiteConfig {
auto_flush_ms: 200,
..LiteConfig::default()
};
let db = NodeDbLite::open_at_path_with_config(&path, Encryption::Plaintext, config)
.await
.expect("open db at path");
db.kv_put("col", "key", b"at_path_flushed")
.await
.expect("kv_put");
tokio::time::sleep(Duration::from_millis(450)).await;
db.shutdown().await;
}
{
let db = NodeDbLite::open_at_path(&path, Encryption::Plaintext)
.await
.expect("reopen db at path");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert_eq!(
got.as_deref(),
Some(b"at_path_flushed".as_slice()),
"open_at_path_with_config must honor auto_flush_ms from the supplied config"
);
}
}
#[tokio::test]
async fn open_with_config_auto_flush_ms_zero_leaves_writes_unflushed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config_auto_flush_zero.pagedb");
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("open storage");
let config = LiteConfig {
auto_flush_ms: 0,
..LiteConfig::default()
};
let db = NodeDbLite::open_with_config(storage, config)
.await
.expect("open db");
db.kv_put("col", "key", b"unflushed").await.expect("kv_put");
tokio::time::sleep(Duration::from_millis(450)).await;
}
{
let storage = PagedbStorageDefault::open(&path, Encryption::Plaintext)
.await
.expect("reopen storage");
let db = NodeDbLite::open(storage).await.expect("reopen db");
let got = db.kv_get("col", "key").await.expect("kv_get after reopen");
assert!(
got.is_none(),
"auto_flush_ms = 0 must disable the background task entirely; got: {got:?}"
);
}
}