diff --git a/radicle-cli/src/commands/issue.rs b/radicle-cli/src/commands/issue.rs index b6cf8dee..347b5a88 100644 --- a/radicle-cli/src/commands/issue.rs +++ b/radicle-cli/src/commands/issue.rs @@ -234,7 +234,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { } Operation::List => { for result in issues.all()? { - let (id, issue) = result?; + let (id, issue, _) = result?; println!("{} {}", id, issue.title()); } } diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 04b4cd21..6edcb001 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -328,7 +328,7 @@ async fn issues_handler( .all()? .into_iter() .filter_map(|r| r.ok()) - .map(|(id, issue)| { + .map(|(id, issue, _)| { json!({ "id": id, "author": issue.author(), diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index 43751baf..6aca2d75 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -77,7 +77,6 @@ pub struct Issue { title: LWWReg, clock::Lamport>, status: LWWReg, clock::Lamport>, thread: Thread, - clock: clock::Lamport, } impl Semilattice for Issue { @@ -94,7 +93,6 @@ impl Default for Issue { title: Max::from(String::default()).into(), status: Max::from(Status::default()).into(), thread: Thread::default(), - clock: clock::Lamport::default(), } } } @@ -104,10 +102,13 @@ impl store::FromHistory for Issue { &*TYPENAME } - fn from_history(history: &radicle_cob::History) -> Result { - Ok(history.traverse(Self::default(), |mut acc, entry| { + fn from_history( + history: &radicle_cob::History, + ) -> Result<(Self, clock::Lamport), store::Error> { + let mut clock = clock::Lamport::default(); + let obj = history.traverse(Self::default(), |mut acc, entry| { if let Ok(change) = Change::decode(entry.contents()) { - if let Err(err) = acc.apply(change) { + if let Err(err) = acc.apply(change, &mut clock) { log::warn!("Error applying change to issue state: {err}"); return ControlFlow::Break(acc); } @@ -115,7 +116,9 @@ impl store::FromHistory for Issue { return ControlFlow::Break(acc); } ControlFlow::Continue(acc) - })) + }); + + Ok((obj, clock)) } } @@ -147,32 +150,31 @@ impl Issue { self.thread.comments().map(|(id, comment)| (id, comment)) } - pub fn clock(&self) -> &clock::Lamport { - &self.clock - } - pub fn timeline(&self) -> Vec { todo!(); } - pub fn apply(&mut self, change: Change) -> Result<(), Error> { + pub fn apply(&mut self, change: Change, clock: &mut clock::Lamport) -> Result<(), Error> { match change.action { Action::Title { title } => { self.title.set(title, change.clock); + clock.merge(change.clock); } Action::Lifecycle { status } => { self.status.set(status, change.clock); + clock.merge(change.clock); } Action::Thread { action } => { - self.thread.apply([crdt::Change { - action, - author: change.author, - clock: change.clock, - }]); + self.thread.apply( + [crdt::Change { + action, + author: change.author, + clock: change.clock, + }], + clock, + ); } } - self.clock.merge(change.clock); - Ok(()) } } @@ -300,7 +302,7 @@ impl<'a, 'g> IssueMut<'a, 'g> { ) -> Result { let id = change.id(); - self.issue.apply(change.clone())?; + self.issue.apply(change.clone(), &mut self.clock)?; self.store .update(self.id, msg, change, signer) .map_err(|e| Error::Store(store::Error::from(e)))?; @@ -342,19 +344,19 @@ impl<'a> Issues<'a> { /// Get an issue. pub fn get(&self, id: &ObjectId) -> Result, store::Error> { - self.raw.get(id) + self.raw.get(id).map(|r| r.map(|(i, _clock)| i)) } /// Get an issue mutably. pub fn get_mut<'g>(&'g mut self, id: &ObjectId) -> Result, store::Error> { - let issue = self + let (issue, clock) = self .raw .get(id)? .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?; Ok(IssueMut { id: *id, - clock: issue.clock, + clock, issue, store: self, }) @@ -375,10 +377,10 @@ impl<'a> Issues<'a> { action: Action::Title { title }, clock: clock::Lamport::default(), }; - let (id, issue): (_, Issue) = self.raw.create("Create issue", change, signer)?; + let (id, issue, clock) = self.raw.create("Create issue", change, signer)?; let mut issue = IssueMut { id, - clock: issue.clock, + clock, issue, store: self, }; @@ -600,13 +602,14 @@ mod test { let issues = issues .all() .unwrap() + .map(|r| r.map(|(_, i, _)| i)) .collect::, _>>() .unwrap(); assert_eq!(issues.len(), 3); - issues.iter().find(|(_, i)| i.title() == "First").unwrap(); - issues.iter().find(|(_, i)| i.title() == "Second").unwrap(); - issues.iter().find(|(_, i)| i.title() == "Third").unwrap(); + issues.iter().find(|i| i.title() == "First").unwrap(); + issues.iter().find(|i| i.title() == "Second").unwrap(); + issues.iter().find(|i| i.title() == "Third").unwrap(); } } diff --git a/radicle/src/cob/store.rs b/radicle/src/cob/store.rs index e4b1febf..7726bcf4 100644 --- a/radicle/src/cob/store.rs +++ b/radicle/src/cob/store.rs @@ -2,7 +2,7 @@ #![allow(clippy::large_enum_variant)] use std::marker::PhantomData; -use radicle_crdt::Change; +use radicle_crdt::{Change, Lamport}; use serde::Serialize; use crate::cob; @@ -21,7 +21,7 @@ pub trait FromHistory: Sized { /// The object type name. fn type_name() -> &'static TypeName; /// Create an object from a history. - fn from_history(history: &History) -> Result; + fn from_history(history: &History) -> Result<(Self, Lamport), Error>; } /// Store error. @@ -107,7 +107,7 @@ impl<'a, T: FromHistory> Store<'a, T> { message: &'static str, change: Change, signer: &G, - ) -> Result<(ObjectId, T), Error> { + ) -> Result<(ObjectId, T, Lamport), Error> { let cob = cob::create( self.raw, signer, @@ -120,30 +120,32 @@ impl<'a, T: FromHistory> Store<'a, T> { contents: change.encode(), }, )?; - let object = T::from_history(cob.history())?; + let (object, clock) = T::from_history(cob.history())?; - Ok((*cob.id(), object)) + Ok((*cob.id(), object, clock)) } /// Get an object. - pub fn get(&self, id: &ObjectId) -> Result, Error> { + pub fn get(&self, id: &ObjectId) -> Result, Error> { let cob = cob::get(self.raw, T::type_name(), id)?; if let Some(cob) = cob { - let obj = T::from_history(cob.history())?; - Ok(Some(obj)) + let (obj, clock) = T::from_history(cob.history())?; + Ok(Some((obj, clock))) } else { Ok(None) } } /// Return all objects. - pub fn all(&self) -> Result>, Error> { + pub fn all( + &self, + ) -> Result>, Error> { let raw = cob::list(self.raw, T::type_name())?; Ok(raw.into_iter().map(|o| { - let obj = T::from_history(o.history())?; - Ok((*o.id(), obj)) + let (obj, clock) = T::from_history(o.history())?; + Ok((*o.id(), obj, clock)) })) } diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index cc39775d..38a5c411 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -87,15 +87,18 @@ impl store::FromHistory for Thread { &*TYPENAME } - fn from_history(history: &History) -> Result { - Ok(history.traverse(Thread::default(), |mut acc, entry| { + fn from_history(history: &History) -> Result<(Self, Lamport), store::Error> { + let mut clock = Lamport::default(); + let obj = history.traverse(Thread::default(), |mut acc, entry| { if let Ok(change) = Change::decode(entry.contents()) { - acc.apply([change]); + acc.apply([change], &mut clock); ControlFlow::Continue(acc) } else { ControlFlow::Break(acc) } - })) + }); + + Ok((obj, clock)) } } @@ -153,7 +156,11 @@ impl Thread { .map(|(a, r)| (a, r)) } - pub fn apply(&mut self, changes: impl IntoIterator>) { + pub fn apply( + &mut self, + changes: impl IntoIterator>, + clock: &mut Lamport, + ) { for change in changes.into_iter() { let id = change.id(); @@ -217,6 +224,7 @@ impl Thread { }); } } + clock.merge(change.clock); } } @@ -413,6 +421,7 @@ mod tests { let (_, signer, repository) = radicle::test::setup::context(&tmp); let store = radicle::cob::store::Store::::open(*signer.public_key(), &repository).unwrap(); + let mut clock = Lamport::default(); let mut alice = Actor::new(signer); @@ -420,14 +429,14 @@ mod tests { let a2 = alice.comment("Second comment", None); let mut expected = Thread::default(); - expected.apply([a1.clone(), a2.clone()]); + expected.apply([a1.clone(), a2.clone()], &mut clock); - let (id, _) = store.create("Thread created", a1, &alice.signer).unwrap(); + let (id, _, _) = store.create("Thread created", a1, &alice.signer).unwrap(); store .update(id, "Thread updated", a2, &alice.signer) .unwrap(); - let actual = store.get(&id).unwrap().unwrap(); + let (actual, _) = store.get(&id).unwrap().unwrap(); assert_eq!(actual, expected); } @@ -506,15 +515,16 @@ mod tests { fn prop_invariants(log: Changes<3>) { let t = Thread::default(); let [p1, p2, p3] = log.permutations; + let mut clock = Lamport::default(); let mut t1 = t.clone(); - t1.apply(p1); + t1.apply(p1, &mut clock); let mut t2 = t.clone(); - t2.apply(p2); + t2.apply(p2, &mut clock); let mut t3 = t; - t3.apply(p3); + t3.apply(p3, &mut clock); assert_eq!(t1, t2); assert_eq!(t2, t3);