feat: Implement DAG-based conflict resolution with binary keys and multi-head CLI display

This commit is contained in:
2025-12-22 01:56:26 +01:00
parent 346ebccee7
commit 57c2906b10
10 changed files with 950 additions and 240 deletions
+60 -12
View File
@@ -67,10 +67,10 @@ pub fn commands() -> Vec<Command> {
},
Command {
name: "get",
args: "<key>",
args: "<key> [-v]",
description: "Retrieve a value by key",
min_args: 1,
max_args: 1,
max_args: 2,
handler: cmd_get,
},
Command {
@@ -240,7 +240,7 @@ fn cmd_put(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) ->
return CommandResult::Ok;
};
let start = Instant::now();
match h.put(&args[0], args[1].as_bytes()) {
match h.put(args[0].as_bytes(), args[1].as_bytes()) {
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
Err(e) => eprintln!("Error: {}", e),
}
@@ -252,14 +252,48 @@ fn cmd_get(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) ->
println!("No store selected. Use 'init' or 'use <uuid>'");
return CommandResult::Ok;
};
let verbose = args.get(1).map(|a| a == "-v").unwrap_or(false);
let start = Instant::now();
match h.get(&args[0]) {
Ok(Some(v)) => {
println!("{}", format_value(&v));
println!("({:.2?})", start.elapsed());
let key = args[0].as_bytes();
if verbose {
// Show all heads
match h.get_heads(key) {
Ok(heads) if heads.is_empty() => println!("(nil)"),
Ok(heads) => {
for (i, head) in heads.iter().enumerate() {
let winner = if i == 0 { "" } else { " " };
let tombstone = if head.tombstone { "" } else { "" };
let author_short = hex::encode(&head.author).chars().take(8).collect::<String>();
if head.tombstone {
println!("{} {} (deleted) (hlc:{}, author:{})",
winner, tombstone, head.hlc, author_short);
} else {
println!("{} {} (hlc:{}, author:{})",
winner, format_value(&head.value), head.hlc, author_short);
}
}
if heads.len() > 1 {
println!("{} heads (conflict)", heads.len());
}
println!("({:.2?})", start.elapsed());
}
Err(e) => eprintln!("Error: {}", e),
}
} else {
match h.get(key) {
Ok(Some(v)) => {
let heads = h.get_heads(key).unwrap_or_default();
if heads.len() > 1 {
println!("{} (⚠ {} heads)", format_value(&v), heads.len());
} else {
println!("{}", format_value(&v));
}
println!("({:.2?})", start.elapsed());
}
Ok(None) => println!("(nil)"),
Err(e) => eprintln!("Error: {}", e),
}
Ok(None) => println!("(nil)"),
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
@@ -270,7 +304,7 @@ fn cmd_delete(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String])
return CommandResult::Ok;
};
let start = Instant::now();
match h.delete(&args[0]) {
match h.delete(args[0].as_bytes()) {
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
Err(e) => eprintln!("Error: {}", e),
}
@@ -290,10 +324,24 @@ fn cmd_list(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -
println!("(empty)");
} else {
for (k, v) in &entries {
let key_str = format_value(k);
if verbose {
println!("{} = {} ({} bytes)", k, format_value(v), v.len());
// Show all heads for this key
let heads = h.get_heads(k).unwrap_or_default();
println!("{}:", key_str);
for (i, head) in heads.iter().enumerate() {
let winner = if i == 0 { "" } else { " " };
let author_short = hex::encode(&head.author).chars().take(8).collect::<String>();
if head.tombstone {
println!(" {} ⊗ (deleted) (hlc:{}, author:{})",
winner, head.hlc, author_short);
} else {
println!(" {} {} (hlc:{}, author:{})",
winner, format_value(&head.value), head.hlc, author_short);
}
}
} else {
println!("{} = {}", k, format_value(v));
println!("{} = {}", key_str, format_value(v));
}
}
println!("({} keys, {:.2?})", entries.len(), start.elapsed());
+30 -14
View File
@@ -189,11 +189,15 @@ pub struct StoreHandle {
impl StoreHandle {
pub fn id(&self) -> Uuid { self.store_id }
pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>, NodeError> {
pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, NodeError> {
Ok(self.store.get(key)?)
}
pub fn list(&self) -> Result<Vec<(String, Vec<u8>)>, NodeError> {
pub fn get_heads(&self, key: &[u8]) -> Result<Vec<lattice_core::HeadInfo>, NodeError> {
Ok(self.store.get_heads(key)?)
}
pub fn list(&self) -> Result<Vec<(Vec<u8>, Vec<u8>)>, NodeError> {
Ok(self.store.list_all()?)
}
@@ -202,18 +206,29 @@ impl StoreHandle {
}
pub fn applied_seq(&self) -> Result<u64, NodeError> {
Ok(self.store.last_seq()?)
let author = self.node.public_key_bytes();
Ok(self.store.author_state(&author)?
.map(|s| s.seq)
.unwrap_or(0))
}
pub fn put(&self, key: &str, value: &[u8]) -> Result<u64, NodeError> {
self.commit_entry(|b| b.put(key, value.to_vec()))
pub fn put(&self, key: &[u8], value: &[u8]) -> Result<u64, NodeError> {
// Get current heads for this key to cite as parents
let heads = self.store.get_heads(key)?;
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
self.commit_entry(parent_hashes, |b| b.put(key.to_vec(), value.to_vec()))
}
pub fn delete(&self, key: &str) -> Result<u64, NodeError> {
self.commit_entry(|b| b.delete(key))
pub fn delete(&self, key: &[u8]) -> Result<u64, NodeError> {
// Get current heads for this key to cite as parents
let heads = self.store.get_heads(key)?;
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
self.commit_entry(parent_hashes, |b| b.delete(key.to_vec()))
}
fn commit_entry<F>(&self, build: F) -> Result<u64, NodeError>
fn commit_entry<F>(&self, parent_hashes: Vec<Vec<u8>>, build: F) -> Result<u64, NodeError>
where
F: FnOnce(EntryBuilder) -> EntryBuilder,
{
@@ -223,7 +238,8 @@ impl StoreHandle {
let builder = EntryBuilder::new(seq, HLC::now())
.store_id(self.store_id.as_bytes().to_vec())
.prev_hash(prev_hash.to_vec());
.prev_hash(prev_hash.to_vec())
.parent_hashes(parent_hashes);
let entry = build(builder).sign(&self.node);
sigchain.append(&entry)?;
@@ -261,8 +277,8 @@ mod tests {
assert!(stores.contains(&store_id));
let (handle, _) = node.open_store(store_id).expect("Failed to open store");
handle.put("/key", b"value").expect("put failed");
assert_eq!(handle.get("/key").unwrap(), Some(b"value".to_vec()));
handle.put(b"/key", b"value").expect("put failed");
assert_eq!(handle.get(b"/key").unwrap(), Some(b"value".to_vec()));
let _ = std::fs::remove_dir_all(data_dir.base());
}
@@ -279,12 +295,12 @@ mod tests {
let store_b = node.create_store().expect("create B");
let (handle_a, _) = node.open_store(store_a).expect("open A");
handle_a.put("/key", b"from A").expect("put A");
handle_a.put(b"/key", b"from A").expect("put A");
let (handle_b, _) = node.open_store(store_b).expect("open B");
assert_eq!(handle_b.get("/key").unwrap(), None);
assert_eq!(handle_b.get(b"/key").unwrap(), None);
assert_eq!(handle_a.get("/key").unwrap(), Some(b"from A".to_vec()));
assert_eq!(handle_a.get(b"/key").unwrap(), Some(b"from A".to_vec()));
let _ = std::fs::remove_dir_all(data_dir.base());
}