feat: Refactor mesh server to use Arc<Node>, introduce JoinAcceptance for mesh joins, and enhance store listing with prefix filtering and deleted entry inclusion.

This commit is contained in:
2025-12-23 00:31:54 +01:00
parent 76810f8d8e
commit 9d4495b3d7
13 changed files with 341 additions and 206 deletions
+78 -3
View File
@@ -247,16 +247,36 @@ impl Store {
}
/// List all key-value pairs (winner values only)
pub fn list_all(&self) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StoreError> {
/// If include_deleted is true, includes tombstoned entries
pub fn list_all(&self, include_deleted: bool) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StoreError> {
self.list_by_prefix(&[], include_deleted)
}
/// List all key-value pairs matching a prefix (winner values only)
/// Uses efficient range query on redb's sorted B-tree
/// If include_deleted is true, includes tombstoned entries
pub fn list_by_prefix(&self, prefix: &[u8], include_deleted: bool) -> Result<Vec<(Vec<u8>, Vec<u8>)>, StoreError> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(KV_TABLE)?;
let mut result = Vec::new();
for entry in table.iter()? {
// Use range query: from prefix to first key that doesn't match
for entry in table.range(prefix..)? {
let (key, value) = entry?;
let key_bytes = key.value();
// Stop when we've passed the prefix
if !key_bytes.starts_with(prefix) {
break;
}
let heads = HeadList::decode(value.value())?.heads;
if let Some(winner) = Self::pick_winner(&heads) {
result.push((key.value().to_vec(), winner.value.clone()));
// Skip tombstones unless include_deleted is true
if include_deleted || !winner.tombstone {
result.push((key_bytes.to_vec(), winner.value.clone()));
}
}
}
Ok(result)
@@ -1672,4 +1692,59 @@ mod tests {
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_list_by_prefix_filters_tombstones() {
let path = temp_db_path("list_tombstones");
let _ = std::fs::remove_file(&path);
let store = Store::open(&path).unwrap();
let node = NodeIdentity::generate();
// Create a key under /test/ prefix
let clock1 = MockClock::new(1000);
let entry1 = EntryBuilder::new(1, HLC::now_with_clock(&clock1))
.store_id(TEST_STORE.to_vec())
.prev_hash([0u8; 32].to_vec())
.put("/test/key1", b"value1".to_vec())
.sign(&node);
store.apply_entry(&entry1).unwrap();
// Create another key
let clock2 = MockClock::new(2000);
let entry2 = EntryBuilder::new(2, HLC::now_with_clock(&clock2))
.store_id(TEST_STORE.to_vec())
.prev_hash(hash_signed_entry(&entry1).to_vec())
.put("/test/key2", b"value2".to_vec())
.sign(&node);
store.apply_entry(&entry2).unwrap();
// Delete key1
let clock3 = MockClock::new(3000);
let entry3 = EntryBuilder::new(3, HLC::now_with_clock(&clock3))
.store_id(TEST_STORE.to_vec())
.prev_hash(hash_signed_entry(&entry2).to_vec())
.parent_hashes(vec![hash_signed_entry(&entry1).to_vec()])
.delete(b"/test/key1")
.sign(&node);
store.apply_entry(&entry3).unwrap();
// list_by_prefix without include_deleted should only show key2
let entries = store.list_by_prefix(b"/test/", false).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, b"/test/key2");
// list_by_prefix with include_deleted should show both (key1 as tombstone)
let entries_all = store.list_by_prefix(b"/test/", true).unwrap();
assert_eq!(entries_all.len(), 2);
// Verify list_all also respects the flag
let all_entries = store.list_all(false).unwrap();
assert_eq!(all_entries.len(), 1);
let all_entries_incl_deleted = store.list_all(true).unwrap();
assert_eq!(all_entries_incl_deleted.len(), 2);
let _ = std::fs::remove_file(&path);
}
}