feat: Implement DAG-based conflict resolution with binary keys and multi-head CLI display
This commit is contained in:
+60
-12
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user