cob: Move clock out of `Issue`

The clock shouldn't be part of the issue state, but rather
the local "actor" state.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-27 12:54:08 +01:00
parent c5225c64cd
commit 09a8b46f34
No known key found for this signature in database
5 changed files with 66 additions and 51 deletions

View File

@ -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());
}
}

View File

@ -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(),

View File

@ -77,7 +77,6 @@ pub struct Issue {
title: LWWReg<Max<String>, clock::Lamport>,
status: LWWReg<Max<Status>, 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<Self, store::Error> {
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<Action> {
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<ChangeId, Error> {
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<Option<Issue>, 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<IssueMut<'a, 'g>, 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::<Result<Vec<_>, _>>()
.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();
}
}

View File

@ -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<Self, Error>;
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<A>,
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<Option<T>, Error> {
pub fn get(&self, id: &ObjectId) -> Result<Option<(T, Lamport)>, 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<impl Iterator<Item = Result<(ObjectId, T), Error>>, Error> {
pub fn all(
&self,
) -> Result<impl Iterator<Item = Result<(ObjectId, T, Lamport), Error>>, 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))
}))
}

View File

@ -87,15 +87,18 @@ impl store::FromHistory for Thread {
&*TYPENAME
}
fn from_history(history: &History) -> Result<Self, store::Error> {
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<Item = Change<Action>>) {
pub fn apply(
&mut self,
changes: impl IntoIterator<Item = Change<Action>>,
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::<Thread>::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);