diff --git a/radicle-cli/src/terminal/format.rs b/radicle-cli/src/terminal/format.rs index 8a6dd918..a74bedae 100644 --- a/radicle-cli/src/terminal/format.rs +++ b/radicle-cli/src/terminal/format.rs @@ -2,10 +2,9 @@ use std::{fmt, time}; pub use dialoguer::console::style; -use radicle::cob::Timestamp; +use radicle::cob::{ObjectId, Timestamp}; use radicle::node::NodeId; use radicle::profile::Profile; -use radicle_cob::ObjectId; use crate::terminal as term; diff --git a/radicle-cob/src/history.rs b/radicle-cob/src/history.rs index 95c89ca8..cf6444c2 100644 --- a/radicle-cob/src/history.rs +++ b/radicle-cob/src/history.rs @@ -89,7 +89,7 @@ impl History { /// Get the current history timestamp. /// This is the latest timestamp of any tip. - pub fn timestamp(&self) -> Clock { + pub fn timestamp(&self) -> Timestamp { self.graph .externals(petgraph::Direction::Outgoing) .map(|n| self.graph[n].timestamp()) diff --git a/radicle-crdt/src/change.rs b/radicle-crdt/src/change.rs index 1605f6be..50301e82 100644 --- a/radicle-crdt/src/change.rs +++ b/radicle-crdt/src/change.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use radicle_crypto::{PublicKey, Signature, Signer}; use serde::{Deserialize, Serialize}; +use crate::clock; use crate::clock::Lamport; /// Identifies a change. @@ -21,6 +22,8 @@ pub struct Change { pub author: ActorId, /// Lamport clock. pub clock: Lamport, + /// Timestamp of this change. + pub timestamp: clock::Physical, } impl Change { @@ -101,10 +104,12 @@ impl Actor { pub fn change(&mut self, action: A) -> Change { let author = *self.signer.public_key(); let clock = self.clock; + let timestamp = clock::Physical::now(); let change = Change { action, author, clock, + timestamp, }; self.changes.insert((self.clock, author), change.clone()); self.clock.tick(); diff --git a/radicle-crdt/src/clock.rs b/radicle-crdt/src/clock.rs index c72d0c1d..4c3a1193 100644 --- a/radicle-crdt/src/clock.rs +++ b/radicle-crdt/src/clock.rs @@ -1,3 +1,6 @@ +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + use num_traits::Bounded; use serde::{Deserialize, Serialize}; @@ -54,3 +57,59 @@ impl Bounded for Lamport { Self::from(u64::max_value()) } } + +/// Physical clock. Tracks real-time by the second. +#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Physical { + seconds: u64, +} + +impl Physical { + pub fn new(seconds: u64) -> Self { + Self { seconds } + } + + pub fn now() -> Self { + #[allow(clippy::unwrap_used)] // Safe because Unix was already invented! + let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + Self { + seconds: duration.as_secs(), + } + } + + pub fn as_secs(&self) -> u64 { + self.seconds + } +} + +impl From for Physical { + fn from(seconds: u64) -> Self { + Self { seconds } + } +} + +impl std::ops::Add for Physical { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self { + seconds: self.seconds + rhs, + } + } +} + +impl Bounded for Physical { + fn min_value() -> Self { + Self { + seconds: u64::min_value(), + } + } + + fn max_value() -> Self { + Self { + seconds: u64::max_value(), + } + } +} diff --git a/radicle/src/cob.rs b/radicle/src/cob.rs index 64d3cd3a..59a1e712 100644 --- a/radicle/src/cob.rs +++ b/radicle/src/cob.rs @@ -4,6 +4,8 @@ pub mod patch; pub mod store; pub mod thread; +pub use radicle_crdt::clock::Physical as Timestamp; + pub use cob::{ identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History, ObjectId, TypeName, Update, diff --git a/radicle/src/cob/common.rs b/radicle/src/cob/common.rs index e7c5b551..29df51b1 100644 --- a/radicle/src/cob/common.rs +++ b/radicle/src/cob/common.rs @@ -1,12 +1,12 @@ use std::fmt; use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; -use num_traits::Bounded; use serde::{Deserialize, Serialize}; use crate::prelude::*; +pub use radicle_crdt::clock::Physical as Timestamp; + /// Author. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Author { @@ -23,55 +23,6 @@ impl Author { } } -#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Timestamp { - seconds: u64, -} - -impl Timestamp { - pub fn new(seconds: u64) -> Self { - Self { seconds } - } - - pub fn now() -> Self { - #[allow(clippy::unwrap_used)] // Safe because Unix was already invented! - let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - - Self { - seconds: duration.as_secs(), - } - } - - pub fn as_secs(&self) -> u64 { - self.seconds - } -} - -impl std::ops::Add for Timestamp { - type Output = Self; - - fn add(self, rhs: u64) -> Self::Output { - Self { - seconds: self.seconds + rhs, - } - } -} - -impl Bounded for Timestamp { - fn min_value() -> Self { - Self { - seconds: u64::min_value(), - } - } - - fn max_value() -> Self { - Self { - seconds: u64::max_value(), - } - } -} - #[derive(thiserror::Error, Debug)] pub enum ReactionError { #[error("invalid reaction")] diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index 17fd5149..40a3fc8b 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -113,6 +113,7 @@ impl store::FromHistory for Issue { action, author: *entry.actor(), clock: entry.clock().into(), + timestamp: entry.timestamp().into(), }) { log::warn!("Error applying change to issue state: {err}"); return ControlFlow::Break(acc); @@ -168,6 +169,7 @@ impl Issue { action, author: change.author, clock: change.clock, + timestamp: change.timestamp, }]); } } @@ -275,16 +277,17 @@ impl<'a, 'g> IssueMut<'a, 'g> { .store .update(self.id, msg, action.clone(), signer) .map_err(Error::Store)?; - let clock = cob.history().clock(); - + let clock = cob.history().clock().into(); + let timestamp = cob.history().timestamp().into(); let change = Change { - author: *signer.public_key(), action, - clock: clock.into(), + author: *signer.public_key(), + clock, + timestamp, }; self.issue.apply(change)?; - Ok((clock.into(), *signer.public_key())) + Ok((clock, *signer.public_key())) } } diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 3f71fad8..cbbdb6d8 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -13,11 +13,10 @@ use radicle_crdt::{ActorId, ChangeId, LWWReg, LWWSet, Max, Redactable, Semilatti use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::cob::common::{Author, Tag}; +use crate::cob::common::{Author, Tag, Timestamp}; use crate::cob::thread; use crate::cob::thread::CommentId; use crate::cob::thread::Thread; -use crate::cob::Timestamp; use crate::cob::{store, ObjectId, TypeName}; use crate::crypto::{PublicKey, Signer}; use crate::git; @@ -31,6 +30,7 @@ pub use clock::Lamport as Clock; pub static TYPENAME: Lazy = Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid")); +/// Patch change. pub type Change = crdt::Change; /// Identifier for a patch. @@ -240,8 +240,7 @@ impl Patch { pub fn apply_one(&mut self, change: Change) -> Result<(), ApplyError> { let id = change.id(); let author = Author::new(change.author); - // FIXME(cloudhead): Use commit timestamp. - let timestamp = Timestamp::default(); + let timestamp = change.timestamp; match change.action { Action::Edit { @@ -318,6 +317,7 @@ impl Patch { action, author: change.author, clock: change.clock, + timestamp, }]); } else { return Err(ApplyError::Missing(revision)); @@ -344,6 +344,7 @@ impl store::FromHistory for Patch { action, author: *entry.actor(), clock: entry.clock().into(), + timestamp: entry.timestamp().into(), }]) { log::warn!("Error applying change to patch state: {err}"); return ControlFlow::Break(acc); @@ -669,20 +670,21 @@ impl<'a, 'g> PatchMut<'a, 'g> { action: Action, signer: &G, ) -> Result { - let change = Change { - author: *signer.public_key(), - action: action.clone(), - clock: self.clock.tick(), - }; - self.patch.apply([change])?; - let cob = self .store - .update(self.id, msg, action, signer) + .update(self.id, msg, action.clone(), signer) .map_err(Error::Store)?; - let clock = cob.history().clock(); + let clock = cob.history().clock().into(); + let timestamp = cob.history().timestamp().into(); + let change = Change { + action, + author: *signer.public_key(), + clock, + timestamp, + }; + self.patch.apply_one(change)?; - Ok((clock.into(), *signer.public_key())) + Ok((clock, *signer.public_key())) } } @@ -868,12 +870,14 @@ mod test { let mut changes = Vec::new(); let mut permutations: [Vec; N] = array::from_fn(|_| Vec::new()); + let timestamp = Timestamp::now() + rng.u64(..60); for (clock, action) in gen.take(g.size()) { changes.push(Change { action, author, clock, + timestamp, }); } diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index 0c40e976..ff876c77 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -8,9 +8,9 @@ use once_cell::sync::Lazy; use radicle_crdt as crdt; use serde::{Deserialize, Serialize}; -use crate::cob::common::{Reaction, Tag}; +use crate::cob::common::{Reaction, Tag, Timestamp}; use crate::cob::store; -use crate::cob::{History, Timestamp, TypeName}; +use crate::cob::{History, TypeName}; use crate::crypto::Signer; use crdt::clock::Lamport; @@ -114,6 +114,7 @@ impl store::FromHistory for Thread { action, author: *entry.actor(), clock: entry.clock().into(), + timestamp: entry.timestamp().into(), }]); ControlFlow::Continue(acc) } else { @@ -187,15 +188,13 @@ impl Thread { } pub fn apply(&mut self, changes: impl IntoIterator>) { - // FIXME(cloudhead): Use commit timestamp. - let timestamp = Timestamp::default(); - for change in changes.into_iter() { let id = change.id(); match change.action { Action::Comment { body, reply_to } => { - let present = Redactable::Present(Comment::new(body, reply_to, timestamp)); + let present = + Redactable::Present(Comment::new(body, reply_to, change.timestamp)); match self.comments.entry(id) { Entry::Vacant(e) => { @@ -436,12 +435,14 @@ mod tests { let mut changes = Vec::new(); let mut permutations: [Vec>; 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 { action, author, clock, + timestamp, }); }