cob: Rename `Change` to `Op`

Helps with the confusion with `radicle_cob::Change` and matches the
name for this object in common literature.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-01 20:29:02 +01:00
parent fa3c155865
commit cc24699232
No known key found for this signature in database
5 changed files with 132 additions and 137 deletions

View File

@ -1,16 +1,16 @@
pub mod change;
pub mod common;
pub mod issue;
pub mod op;
pub mod patch;
pub mod store;
pub mod thread;
pub use change::{Actor, ActorId, Change, ChangeId};
pub use cob::{create, get, list, remove, update};
pub use cob::{
identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History,
ObjectId, TypeName, Update,
};
pub use common::*;
pub use op::{Actor, ActorId, Op, OpId};
use radicle_cob as cob;

View File

@ -11,12 +11,12 @@ use crate::cob;
use crate::cob::common::{Author, Reaction, Tag};
use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread};
use crate::cob::{store, ChangeId, ObjectId, TypeName};
use crate::cob::{store, ObjectId, OpId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::storage::git as storage;
/// Issue change.
pub type Change = crate::cob::Change<Action>;
/// Issue operation.
pub type Op = crate::cob::Op<Action>;
/// Type name of an issue.
pub static TYPENAME: Lazy<TypeName> =
@ -111,9 +111,9 @@ impl store::FromHistory for Issue {
history: &radicle_cob::History,
) -> Result<(Self, clock::Lamport), store::Error> {
let obj = history.traverse(Self::default(), |mut acc, entry| {
if let Ok(change) = Change::try_from(entry) {
if let Err(err) = acc.apply(change) {
log::warn!("Error applying change to issue state: {err}");
if let Ok(op) = Op::try_from(entry) {
if let Err(err) = acc.apply(op) {
log::warn!("Error applying op to issue state: {err}");
return ControlFlow::Break(acc);
}
} else {
@ -154,28 +154,28 @@ impl Issue {
self.thread.comments().map(|(id, comment)| (id, comment))
}
pub fn apply(&mut self, change: Change) -> Result<(), Error> {
match change.action {
pub fn apply(&mut self, op: Op) -> Result<(), Error> {
match op.action {
Action::Title { title } => {
self.title.set(title, change.clock);
self.title.set(title, op.clock);
}
Action::Lifecycle { status } => {
self.status.set(status, change.clock);
self.status.set(status, op.clock);
}
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, change.clock);
self.tags.insert(tag, op.clock);
}
for tag in remove {
self.tags.remove(tag, change.clock);
self.tags.remove(tag, op.clock);
}
}
Action::Thread { action } => {
self.thread.apply([cob::Change {
self.thread.apply([cob::Op {
action,
author: change.author,
clock: change.clock,
timestamp: change.timestamp,
author: op.author,
clock: op.clock,
timestamp: op.timestamp,
}]);
}
}
@ -205,7 +205,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
}
/// Lifecycle an issue.
pub fn lifecycle<G: Signer>(&mut self, status: Status, signer: &G) -> Result<ChangeId, Error> {
pub fn lifecycle<G: Signer>(&mut self, status: Status, signer: &G) -> Result<OpId, Error> {
let action = Action::Lifecycle { status };
self.apply("Lifecycle", action, signer)
}
@ -230,7 +230,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
add: impl IntoIterator<Item = Tag>,
remove: impl IntoIterator<Item = Tag>,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
let action = Action::Tag { add, remove };
@ -244,7 +244,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
parent: CommentId,
body: S,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let body = body.into();
assert!(self.thread.comment(&parent).is_some());
@ -262,7 +262,7 @@ impl<'a, 'g> IssueMut<'a, 'g> {
to: CommentId,
reaction: Reaction,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let action = Action::Thread {
action: thread::Action::React {
to,
@ -273,26 +273,26 @@ impl<'a, 'g> IssueMut<'a, 'g> {
self.apply("React", action, signer)
}
/// Apply a change to the issue.
/// Apply an op to the issue.
pub fn apply<G: Signer>(
&mut self,
msg: &'static str,
action: Action,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let cob = self
.store
.update(self.id, msg, action.clone(), signer)
.map_err(Error::Store)?;
let clock = cob.history().clock().into();
let timestamp = cob.history().timestamp().into();
let change = Change {
let op = Op {
action,
author: *signer.public_key(),
clock,
timestamp,
};
self.issue.apply(change)?;
self.issue.apply(op)?;
Ok((clock, *signer.public_key()))
}

View File

@ -8,40 +8,41 @@ use radicle_crdt::clock;
use radicle_crdt::clock::Lamport;
use radicle_crypto::{PublicKey, Signer};
/// Identifies a change.
pub type ChangeId = (Lamport, ActorId);
/// The author of a change.
/// Identifies an [`Op`].
pub type OpId = (Lamport, ActorId);
/// The author of an [`Op`].
pub type ActorId = PublicKey;
/// Error decoding a change from an entry.
/// Error decoding an operation from an entry.
#[derive(Error, Debug)]
pub enum ChangeDecodeError {
pub enum OpDecodeError {
#[error("deserialization from json failed: {0}")]
Deserialize(#[from] serde_json::Error),
}
/// The `Change` is the unit of replication.
/// Everything that can be done in the system is represented by a `Change` object.
/// Changes are applied to an accumulator to yield a final state.
/// The `Op` is the operation that is applied onto a state to form a CRDT.
///
/// Everything that can be done in the system is represented by an `Op`.
/// Operations are applied to an accumulator to yield a final state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Change<A> {
/// The action carried out by this change.
pub struct Op<A> {
/// The action carried out by this operation.
pub action: A,
/// The author of the change.
/// The author of the operation.
pub author: ActorId,
/// Lamport clock.
pub clock: Lamport,
/// Timestamp of this change.
/// Timestamp of this operation.
pub timestamp: clock::Physical,
}
impl<'a: 'de, 'de, A: serde::Deserialize<'de>> TryFrom<&'a EntryWithClock> for Change<A> {
type Error = ChangeDecodeError;
impl<'a: 'de, 'de, A: serde::Deserialize<'de>> TryFrom<&'a EntryWithClock> for Op<A> {
type Error = OpDecodeError;
fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
let action = serde_json::from_slice(entry.contents())?;
Ok(Change {
Ok(Op {
action,
author: *entry.actor(),
clock: entry.clock().into(),
@ -50,26 +51,20 @@ impl<'a: 'de, 'de, A: serde::Deserialize<'de>> TryFrom<&'a EntryWithClock> for C
}
}
impl<A> Change<A> {
/// Get the change id.
pub fn id(&self) -> ChangeId {
impl<A> Op<A> {
/// Get the op id.
/// This uniquely identifies each operation in the CRDT.
pub fn id(&self) -> OpId {
(self.clock, self.author)
}
}
impl<'de, A: Deserialize<'de>> Change<A> {
/// Deserialize a change from a byte string.
pub fn decode(bytes: &'de [u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
}
/// An object that can be used to create and sign changes.
/// An object that can be used to create and sign operations.
#[derive(Default)]
pub struct Actor<G, A> {
pub signer: G,
pub clock: Lamport,
pub changes: BTreeMap<(Lamport, PublicKey), Change<A>>,
pub ops: BTreeMap<(Lamport, PublicKey), Op<A>>,
}
impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
@ -77,15 +72,15 @@ impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
Self {
signer,
clock: Lamport::default(),
changes: BTreeMap::default(),
ops: BTreeMap::default(),
}
}
pub fn receive(&mut self, changes: impl IntoIterator<Item = Change<A>>) -> Lamport {
for change in changes {
let clock = change.clock;
pub fn receive(&mut self, ops: impl IntoIterator<Item = Op<A>>) -> Lamport {
for op in ops {
let clock = op.clock;
self.changes.insert((clock, change.author), change);
self.ops.insert((clock, op.author), op);
self.clock.merge(clock);
}
self.clock
@ -93,29 +88,29 @@ impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
/// Reset actor state to initial state.
pub fn reset(&mut self) {
self.changes.clear();
self.ops.clear();
self.clock = Lamport::default();
}
/// Returned an ordered list of events.
pub fn timeline(&self) -> impl Iterator<Item = &Change<A>> {
self.changes.values()
pub fn timeline(&self) -> impl Iterator<Item = &Op<A>> {
self.ops.values()
}
/// Create a new change.
pub fn change(&mut self, action: A) -> Change<A> {
/// Create a new operation.
pub fn op(&mut self, action: A) -> Op<A> {
let author = *self.signer.public_key();
let clock = self.clock;
let timestamp = clock::Physical::now();
let change = Change {
let op = Op {
action,
author,
clock,
timestamp,
};
self.changes.insert((self.clock, author), change.clone());
self.ops.insert((self.clock, author), op.clone());
self.clock.tick();
change
op
}
}

View File

@ -16,27 +16,27 @@ use crate::cob::common::{Author, Tag, Timestamp};
use crate::cob::thread;
use crate::cob::thread::CommentId;
use crate::cob::thread::Thread;
use crate::cob::{store, ActorId, ChangeId, ObjectId, TypeName};
use crate::cob::{store, ActorId, ObjectId, OpId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::git;
use crate::prelude::*;
use crate::storage::git as storage;
/// The logical clock we use to order changes to patches.
/// The logical clock we use to order operations to patches.
pub use clock::Lamport as Clock;
/// Type name of a patch.
pub static TYPENAME: Lazy<TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));
/// Patch change.
pub type Change = crate::cob::Change<Action>;
/// Patch operation.
pub type Op = crate::cob::Op<Action>;
/// Identifier for a patch.
pub type PatchId = ObjectId;
/// Unique identifier for a patch revision.
pub type RevisionId = ChangeId;
pub type RevisionId = OpId;
/// Index of a revision in the revisions list.
pub type RevisionIx = usize;
@ -49,10 +49,10 @@ pub enum ApplyError {
/// This error indicates that the operations are not being applied
/// in causal order, which is a requirement for this CRDT.
///
/// For example, this can occur if a change references another change
/// For example, this can occur if an operation references anothern operation
/// that hasn't happened yet.
#[error("causal dependency {0:?} missing")]
Missing(ChangeId),
Missing(OpId),
}
/// Error updating or creating patches.
@ -221,36 +221,36 @@ impl Patch {
matches!(self.status.get().get(), &Status::Archived)
}
/// Apply a list of changes to the state.
pub fn apply(&mut self, changes: impl IntoIterator<Item = Change>) -> Result<(), ApplyError> {
for change in changes {
self.apply_one(change)?;
/// Apply a list of operations to the state.
pub fn apply(&mut self, ops: impl IntoIterator<Item = Op>) -> Result<(), ApplyError> {
for op in ops {
self.apply_one(op)?;
}
Ok(())
}
/// Apply a single change to the state.
pub fn apply_one(&mut self, change: Change) -> Result<(), ApplyError> {
let id = change.id();
let author = Author::new(change.author);
let timestamp = change.timestamp;
/// Apply a single op to the state.
pub fn apply_one(&mut self, op: Op) -> Result<(), ApplyError> {
let id = op.id();
let author = Author::new(op.author);
let timestamp = op.timestamp;
match change.action {
match op.action {
Action::Edit {
title,
description,
target,
} => {
self.title.set(title, change.clock);
self.description.set(description, change.clock);
self.target.set(target, change.clock);
self.title.set(title, op.clock);
self.description.set(description, op.clock);
self.target.set(target, op.clock);
}
Action::Tag { add, remove } => {
for tag in add {
self.tags.insert(tag, change.clock);
self.tags.insert(tag, op.clock);
}
for tag in remove {
self.tags.remove(tag, change.clock);
self.tags.remove(tag, op.clock);
}
}
Action::Revision { base, oid } => {
@ -274,7 +274,7 @@ impl Patch {
} => {
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
revision.reviews.insert(
change.author,
op.author,
Review::new(verdict, comment.to_owned(), inline.to_owned(), timestamp),
);
} else {
@ -285,12 +285,12 @@ impl Patch {
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
revision.merges.insert(
Merge {
node: change.author,
node: op.author,
commit,
timestamp,
}
.into(),
change.clock,
op.clock,
);
} else {
return Err(ApplyError::Missing(revision));
@ -300,10 +300,10 @@ impl Patch {
// TODO(cloudhead): Make sure we can deal with redacted revisions which are added
// to out of order, like in the `Merge` case.
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
revision.discussion.apply([cob::Change {
revision.discussion.apply([cob::Op {
action,
author: change.author,
clock: change.clock,
author: op.author,
clock: op.clock,
timestamp,
}]);
} else {
@ -326,9 +326,9 @@ impl store::FromHistory for Patch {
history: &radicle_cob::History,
) -> Result<(Self, clock::Lamport), store::Error> {
let obj = history.traverse(Self::default(), |mut acc, entry| {
if let Ok(change) = Change::try_from(entry) {
if let Err(err) = acc.apply([change]) {
log::warn!("Error applying change to patch state: {err}");
if let Ok(op) = Op::try_from(entry) {
if let Err(err) = acc.apply([op]) {
log::warn!("Error applying op to patch state: {err}");
return ControlFlow::Break(acc);
}
} else {
@ -554,7 +554,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
description: String,
target: MergeTarget,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let action = Action::Edit {
title,
description,
@ -589,7 +589,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
comment: Option<String>,
inline: Vec<CodeComment>,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let action = Action::Review {
revision,
comment,
@ -605,7 +605,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
revision: RevisionId,
commit: git::Oid,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let action = Action::Merge { revision, commit };
self.apply("Merge revision", action, signer)
}
@ -617,7 +617,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
base: impl Into<git::Oid>,
oid: impl Into<git::Oid>,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let description = description.into();
let base = base.into();
let oid = oid.into();
@ -637,7 +637,7 @@ impl<'a, 'g> PatchMut<'a, 'g> {
add: impl IntoIterator<Item = Tag>,
remove: impl IntoIterator<Item = Tag>,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let add = add.into_iter().collect::<Vec<_>>();
let remove = remove.into_iter().collect::<Vec<_>>();
let action = Action::Tag { add, remove };
@ -645,26 +645,26 @@ impl<'a, 'g> PatchMut<'a, 'g> {
self.apply("Tag", action, signer)
}
/// Apply a change to the patch.
/// Apply an operation to the patch.
pub fn apply<G: Signer>(
&mut self,
msg: &'static str,
action: Action,
signer: &G,
) -> Result<ChangeId, Error> {
) -> Result<OpId, Error> {
let cob = self
.store
.update(self.id, msg, action.clone(), signer)
.map_err(Error::Store)?;
let clock = cob.history().clock().into();
let timestamp = cob.history().timestamp().into();
let change = Change {
let op = Op {
action,
author: *signer.public_key(),
clock,
timestamp,
};
self.patch.apply_one(change)?;
self.patch.apply_one(op)?;
Ok((clock, *signer.public_key()))
}
@ -781,13 +781,13 @@ mod test {
use quickcheck::{Arbitrary, TestResult};
use super::*;
use crate::cob::change::{Actor, ActorId};
use crate::cob::op::{Actor, ActorId};
use crate::crypto::test::signer::MockSigner;
use crate::test;
#[derive(Clone)]
struct Changes<const N: usize> {
permutations: [Vec<Change>; N],
permutations: [Vec<Op>; N],
}
impl<const N: usize> std::fmt::Debug for Changes<N> {
@ -805,7 +805,7 @@ mod test {
impl<const N: usize> Arbitrary for Changes<N> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
type State = (clock::Lamport, Vec<ChangeId>, Vec<Tag>);
type State = (clock::Lamport, Vec<OpId>, Vec<Tag>);
let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
@ -875,11 +875,11 @@ mod test {
});
let mut changes = Vec::new();
let mut permutations: [Vec<Change>; N] = array::from_fn(|_| Vec::new());
let mut permutations: [Vec<Op>; N] = array::from_fn(|_| Vec::new());
let timestamp = Timestamp::now() + rng.u64(..60);
for (clock, action) in gen.take(g.size()) {
changes.push(Change {
changes.push(Op {
action,
author,
clock,
@ -1054,15 +1054,15 @@ mod test {
let mut alice = Actor::<_, Action>::new(MockSigner::default());
let mut patch = Patch::default();
let a1 = alice.change(Action::Revision { base, oid });
let a2 = alice.change(Action::Redact { revision: a1.id() });
let a3 = alice.change(Action::Review {
let a1 = alice.op(Action::Revision { base, oid });
let a2 = alice.op(Action::Redact { revision: a1.id() });
let a3 = alice.op(Action::Review {
revision: a1.id(),
comment: None,
verdict: Some(Verdict::Accept),
inline: vec![],
});
let a4 = alice.change(Action::Merge {
let a4 = alice.op(Action::Merge {
revision: a1.id(),
commit: oid,
});
@ -1085,8 +1085,8 @@ mod test {
let mut p1 = Patch::default();
let mut p2 = Patch::default();
let a1 = alice.change(Action::Revision { base, oid });
let a2 = alice.change(Action::Redact { revision: a1.id() });
let a1 = alice.op(Action::Revision { base, oid });
let a2 = alice.op(Action::Redact { revision: a1.id() });
p1.apply([a1.clone(), a2.clone(), a1.clone()]).unwrap();
p2.apply([a1.clone(), a1, a2]).unwrap();

View File

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use crate::cob;
use crate::cob::common::{Reaction, Timestamp};
use crate::cob::store;
use crate::cob::{ActorId, Change, ChangeId, History, TypeName};
use crate::cob::{ActorId, History, Op, OpId, TypeName};
use crate::crypto::Signer;
use crdt::clock::Lamport;
@ -24,7 +24,7 @@ pub static TYPENAME: Lazy<TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.thread").expect("type name is valid"));
/// Identifies a comment.
pub type CommentId = ChangeId;
pub type CommentId = OpId;
/// A comment on a discussion thread.
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -32,14 +32,14 @@ pub struct Comment {
/// The comment body.
pub body: String,
/// Thread or comment this is a reply to.
pub reply_to: Option<ChangeId>,
pub reply_to: Option<OpId>,
/// When the comment was authored.
pub timestamp: Timestamp,
}
impl Comment {
/// Create a new comment.
pub fn new(body: String, reply_to: Option<ChangeId>, timestamp: Timestamp) -> Self {
pub fn new(body: String, reply_to: Option<OpId>, timestamp: Timestamp) -> Self {
Self {
body,
reply_to,
@ -66,13 +66,13 @@ pub enum Action {
/// Comment body.
body: String,
/// Another comment this is a reply to.
reply_to: Option<ChangeId>,
reply_to: Option<OpId>,
},
/// Redact a change. Not all changes can be redacted.
Redact { id: ChangeId },
Redact { id: OpId },
/// React to a change.
React {
to: ChangeId,
to: OpId,
reaction: Reaction,
active: bool,
},
@ -96,7 +96,7 @@ impl store::FromHistory for Thread {
fn from_history(history: &History) -> Result<(Self, Lamport), store::Error> {
let obj = history.traverse(Thread::default(), |mut acc, entry| {
if let Ok(change) = Change::try_from(entry) {
if let Ok(change) = Op::try_from(entry) {
acc.apply([change]);
ControlFlow::Continue(acc)
} else {
@ -168,7 +168,7 @@ 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 = Op<Action>>) {
for change in changes.into_iter() {
let id = change.id();
@ -257,16 +257,16 @@ impl<G: Signer> Actor<G> {
}
/// Create a new comment.
pub fn comment(&mut self, body: &str, reply_to: Option<ChangeId>) -> Change<Action> {
self.change(Action::Comment {
pub fn comment(&mut self, body: &str, reply_to: Option<OpId>) -> Op<Action> {
self.op(Action::Comment {
body: String::from(body),
reply_to,
})
}
/// Create a new redaction.
pub fn redact(&mut self, id: ChangeId) -> Change<Action> {
self.change(Action::Redact { id })
pub fn redact(&mut self, id: OpId) -> Op<Action> {
self.op(Action::Redact { id })
}
}
@ -299,7 +299,7 @@ mod tests {
#[derive(Clone)]
struct Changes<const N: usize> {
permutations: [Vec<Change<Action>>; N],
permutations: [Vec<Op<Action>>; N],
}
impl<const N: usize> std::fmt::Debug for Changes<N> {
@ -320,7 +320,7 @@ mod tests {
let author = ActorId::from([0; 32]);
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let gen =
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<ChangeId>)>::new(rng.clone())
WeightedGenerator::<(Lamport, Action), (Lamport, Vec<OpId>)>::new(rng.clone())
.variant(3, |(clock, changes), rng| {
changes.push((clock.tick(), author));
@ -357,11 +357,11 @@ mod tests {
});
let mut changes = Vec::new();
let mut permutations: [Vec<Change<Action>>; N] = array::from_fn(|_| Vec::new());
let mut permutations: [Vec<Op<Action>>; N] = array::from_fn(|_| Vec::new());
let timestamp = Timestamp::now() + rng.u64(..60);
for (clock, action) in gen.take(g.size().min(8)) {
changes.push(Change {
changes.push(Op {
action,
author,
clock,
@ -493,9 +493,9 @@ mod tests {
eve.receive([a2.clone()]);
bob.receive([a2.clone()]);
assert_eq!(alice.changes.len(), 7);
assert_eq!(bob.changes.len(), 7);
assert_eq!(eve.changes.len(), 7);
assert_eq!(alice.ops.len(), 7);
assert_eq!(bob.ops.len(), 7);
assert_eq!(eve.ops.len(), 7);
assert_eq!(
bob.timeline().collect::<Vec<_>>(),