use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum CrdtFormat {
Yjs,
Automerge,
DiamondTypes,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CrdtMetadata {
pub clock: std::collections::HashMap<String, u64>,
pub origin: String,
pub seq: u64,
}
impl CrdtMetadata {
#[must_use]
pub fn new(origin: impl Into<String>, seq: u64) -> Self {
let origin = origin.into();
let mut clock = std::collections::HashMap::new();
clock.insert(origin.clone(), seq);
Self { clock, origin, seq }
}
pub fn increment(&mut self, site_id: &str) {
let count = self.clock.entry(site_id.to_string()).or_insert(0);
*count += 1;
if site_id == self.origin {
self.seq = *count;
}
}
pub fn merge(&mut self, other: &Self) {
for (site, &count) in &other.clock {
let entry = self.clock.entry(site.clone()).or_insert(0);
*entry = (*entry).max(count);
}
}
#[must_use]
pub fn happened_before(&self, other: &Self) -> bool {
let mut dominated = false;
for (site, &self_count) in &self.clock {
let other_count = other.clock.get(site).copied().unwrap_or(0);
if self_count > other_count {
return false;
}
if self_count < other_count {
dominated = true;
}
}
for (site, &other_count) in &other.clock {
if !self.clock.contains_key(site) && other_count > 0 {
dominated = true;
}
}
dominated
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextCrdtMetadata {
pub positions: Vec<TextCrdtPosition>,
}
impl TextCrdtMetadata {
#[must_use]
pub fn new() -> Self {
Self {
positions: Vec::new(),
}
}
#[must_use]
pub fn from_text(text: &str, site_id: &str) -> Self {
let positions = text
.chars()
.enumerate()
.map(|(i, c)| TextCrdtPosition {
id: format!("{site_id}:{}", i + 1),
char: c,
})
.collect();
Self { positions }
}
#[must_use]
pub fn text(&self) -> String {
self.positions.iter().map(|p| p.char).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.positions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
}
impl Default for TextCrdtMetadata {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextCrdtPosition {
pub id: String,
pub char: char,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncState {
pub crdt_format: CrdtFormat,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crdt_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_version: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_sync: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub peers: Vec<Peer>,
}
impl SyncState {
#[must_use]
pub fn new(crdt_format: CrdtFormat) -> Self {
Self {
crdt_format,
crdt_version: None,
sync_version: None,
last_sync: None,
peers: Vec::new(),
}
}
#[must_use]
pub fn yjs() -> Self {
Self::new(CrdtFormat::Yjs)
}
#[must_use]
pub fn automerge() -> Self {
Self::new(CrdtFormat::Automerge)
}
#[must_use]
pub fn diamond_types() -> Self {
Self::new(CrdtFormat::DiamondTypes)
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.crdt_version = Some(version.into());
self
}
#[must_use]
pub fn with_sync_version(mut self, version: u64) -> Self {
self.sync_version = Some(version);
self
}
pub fn mark_synced(&mut self) {
self.last_sync = Some(Utc::now());
}
pub fn add_peer(&mut self, peer: Peer) {
if let Some(existing) = self.peers.iter_mut().find(|p| p.id == peer.id) {
existing.last_seen = peer.last_seen;
} else {
self.peers.push(peer);
}
}
pub fn remove_peer(&mut self, peer_id: &str) {
self.peers.retain(|p| p.id != peer_id);
}
#[must_use]
pub fn active_peers(&self, within: chrono::Duration) -> Vec<&Peer> {
let cutoff = Utc::now() - within;
self.peers.iter().filter(|p| p.last_seen > cutoff).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Peer {
pub id: String,
pub last_seen: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Peer {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
last_seen: Utc::now(),
name: None,
}
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn touch(&mut self) {
self.last_seen = Utc::now();
}
}