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