use std::sync::Arc;
use std::thread;
use taladb::Database;
use taladb::document::Value;
use taladb::query::filter::Filter;
use taladb::watch::{create_watch, new_registry, notify};
fn s(v: &str) -> Value {
Value::Str(v.to_string())
}
fn i(n: i64) -> Value {
Value::Int(n)
}
#[test]
fn try_next_returns_none_when_no_event() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let handle = create_watch(®istry, Filter::All, move |filter| {
db_clone.collection("users").unwrap().find(filter.clone())
});
let result = handle.try_next().unwrap();
assert!(result.is_none());
}
#[test]
fn next_returns_snapshot_after_write() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let handle = create_watch(®istry, Filter::All, move |filter| {
db_clone.collection("items").unwrap().find(filter.clone())
});
notify(®istry);
let snapshot = handle.next().unwrap();
assert!(snapshot.is_empty());
db.collection("items")
.unwrap()
.insert(vec![("x".into(), i(1))])
.unwrap();
notify(®istry);
let snapshot = handle.next().unwrap();
assert_eq!(snapshot.len(), 1);
}
#[test]
fn try_next_returns_snapshot_after_notify() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let handle = create_watch(®istry, Filter::All, move |filter| {
db_clone.collection("data").unwrap().find(filter.clone())
});
db.collection("data")
.unwrap()
.insert(vec![("v".into(), i(42))])
.unwrap();
notify(®istry);
let result = handle.try_next().unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn rapid_writes_coalesce() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let col = db.collection("data").unwrap();
let handle = create_watch(®istry, Filter::All, move |filter| {
db_clone.collection("data").unwrap().find(filter.clone())
});
for n in 0..5i64 {
col.insert(vec![("n".into(), i(n))]).unwrap();
notify(®istry);
}
let snapshot = handle.next().unwrap();
assert_eq!(
snapshot.len(),
5,
"coalesced snapshot should see all 5 docs"
);
let pending = handle.try_next().unwrap();
assert!(
pending.is_none(),
"no more events should be pending after drain"
);
}
#[test]
fn watch_filter_applied_to_snapshots() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let col = db.collection("items").unwrap();
col.insert(vec![("status".into(), s("active"))]).unwrap();
col.insert(vec![("status".into(), s("inactive"))]).unwrap();
let handle = create_watch(
®istry,
Filter::Eq("status".into(), s("active")),
move |filter| db_clone.collection("items").unwrap().find(filter.clone()),
);
notify(®istry);
let snapshot = handle.next().unwrap();
assert_eq!(snapshot.len(), 1, "snapshot must respect the watch filter");
assert_eq!(snapshot[0].get("status"), Some(&s("active")));
}
#[test]
fn multiple_subscribers_all_receive_event() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db1 = Arc::clone(&db);
let h1 = create_watch(®istry, Filter::All, move |f| {
db1.collection("col").unwrap().find(f.clone())
});
let db2 = Arc::clone(&db);
let h2 = create_watch(®istry, Filter::All, move |f| {
db2.collection("col").unwrap().find(f.clone())
});
db.collection("col")
.unwrap()
.insert(vec![("x".into(), i(1))])
.unwrap();
notify(®istry);
let s1 = h1.next().unwrap();
let s2 = h2.next().unwrap();
assert_eq!(s1.len(), 1, "subscriber 1 should receive event");
assert_eq!(s2.len(), 1, "subscriber 2 should receive event");
}
#[test]
fn iter_yields_successive_snapshots() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let _col = db.collection("log").unwrap();
let handle = create_watch(®istry, Filter::All, move |f| {
db_clone.collection("log").unwrap().find(f.clone())
});
let registry_clone = Arc::clone(®istry);
let col_thread = db.collection("log").unwrap();
let (consumed_tx, consumed_rx) = std::sync::mpsc::channel::<()>();
let writer = thread::spawn(move || {
for n in 0i64..3 {
col_thread.insert(vec![("n".into(), i(n))]).unwrap();
notify(®istry_clone);
if consumed_rx.recv().is_err() {
break;
}
}
});
let mut count = 0;
for snapshot in handle.iter() {
count += 1;
let _ = snapshot.unwrap();
let _ = consumed_tx.send(());
if count == 3 {
break;
}
}
writer.join().unwrap();
assert_eq!(count, 3, "iter should yield one snapshot per notify");
}
#[test]
fn watch_closed_after_registry_dropped() {
let db = Arc::new(Database::open_in_memory().unwrap());
let registry = new_registry();
let db_clone = Arc::clone(&db);
let handle = create_watch(®istry, Filter::All, move |f| {
db_clone.collection("x").unwrap().find(f.clone())
});
notify(®istry);
let _ = handle.next().unwrap();
drop(registry);
let result = handle.try_next();
match result {
Err(taladb::TalaDbError::WatchClosed) => {}
Ok(None) => {}
other => panic!("expected WatchClosed or None, got: {other:?}"),
}
}
#[test]
fn collection_watch_receives_snapshots_after_writes() {
use taladb::Value;
use taladb::collection::Update;
let db = taladb::Database::open_in_memory().unwrap();
let col = db.collection("tasks").unwrap();
let handle = col.watch(Filter::Eq("done".into(), Value::Bool(false)));
let col2 = db.collection("tasks").unwrap();
col2.insert(vec![
("title".into(), Value::Str("write tests".into())),
("done".into(), Value::Bool(false)),
])
.unwrap();
let snapshot = handle.next().unwrap();
assert_eq!(snapshot.len(), 1);
assert_eq!(
snapshot[0].get("title"),
Some(&Value::Str("write tests".into()))
);
col2.update_one(
Filter::Eq("done".into(), Value::Bool(false)),
Update::Set(vec![("done".into(), Value::Bool(true))]),
)
.unwrap();
let snapshot = handle.next().unwrap();
assert!(snapshot.is_empty());
}
#[test]
fn collection_watch_try_next_sees_deletes() {
use taladb::Value;
let db = taladb::Database::open_in_memory().unwrap();
let col = db.collection("items").unwrap();
let id = col.insert(vec![("x".into(), Value::Int(1))]).unwrap();
let handle = col.watch(Filter::All);
assert!(handle.try_next().unwrap().is_none(), "no writes yet");
col.delete_by_id(id).unwrap();
let snapshot = handle
.try_next()
.unwrap()
.expect("delete must notify watchers");
assert!(snapshot.is_empty());
}
#[test]
fn a_burst_larger_than_the_channel_keeps_the_watch_alive() {
let db = Database::open_in_memory().unwrap();
let col = db.collection("burst").unwrap();
let handle = col.watch(Filter::All);
for n in 0..200i64 {
col.insert(vec![("n".into(), i(n))]).unwrap();
}
let snapshot = handle.next().expect("the watch must survive the burst");
assert_eq!(
snapshot.len(),
200,
"one coalesced snapshot of the latest state"
);
assert!(
handle.try_next().unwrap().is_none(),
"the burst was drained"
);
col.insert(vec![("n".into(), i(200))]).unwrap();
assert_eq!(handle.next().unwrap().len(), 201, "and it keeps delivering");
}
#[test]
fn next_timeout_returns_none_without_a_write_and_a_snapshot_after_one() {
use std::time::Duration;
let db = Database::open_in_memory().unwrap();
let col = db.collection("timed").unwrap();
let handle = col.watch(Filter::All);
assert!(
handle
.next_timeout(Duration::from_millis(20))
.unwrap()
.is_none()
);
col.insert(vec![("name".into(), s("a"))]).unwrap();
let snapshot = handle
.next_timeout(Duration::from_millis(20))
.unwrap()
.expect("a write before the call is delivered");
assert_eq!(snapshot.len(), 1);
}