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,
});
}