tauri-plugin-pg-sync 0.1.11

Offline-first PostgreSQL sync plugin for Tauri apps
use chrono::Utc;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct HybridLogicalClock {
    pub physical: u64,
    pub logical: u32,
    pub node_id: String,
}

impl HybridLogicalClock {
    pub fn new(node_id: String) -> Self {
        Self {
            physical: Self::now_millis(),
            logical: 0,
            node_id,
        }
    }

    fn now_millis() -> u64 {
        Utc::now().timestamp_millis() as u64
    }

    pub fn tick(&mut self) -> String {
        let now = Self::now_millis();

        if now > self.physical {
            self.physical = now;
            self.logical = 0;
        } else {
            self.logical += 1;
        }

        self.to_string()
    }

    pub fn update(&mut self, remote_hlc: &str) -> String {
        if let Some(remote) = Self::parse(remote_hlc) {
            let now = Self::now_millis();

            if now > self.physical && now > remote.physical {
                self.physical = now;
                self.logical = 0;
            } else if self.physical == remote.physical {
                self.logical = self.logical.max(remote.logical) + 1;
            } else if self.physical > remote.physical {
                self.logical += 1;
            } else {
                self.physical = remote.physical;
                self.logical = remote.logical + 1;
            }
        } else {
            self.tick();
        }

        self.to_string()
    }

    pub fn parse(s: &str) -> Option<Self> {
        let parts: Vec<&str> = s.split(':').collect();
        if parts.len() != 3 {
            return None;
        }

        Some(Self {
            physical: parts[0].parse().ok()?,
            logical: parts[1].parse().ok()?,
            node_id: parts[2].to_string(),
        })
    }

    pub fn compare(a: &str, b: &str) -> std::cmp::Ordering {
        let hlc_a = Self::parse(a);
        let hlc_b = Self::parse(b);

        match (hlc_a, hlc_b) {
            (Some(a), Some(b)) => a.cmp(&b),
            (Some(_), None) => std::cmp::Ordering::Greater,
            (None, Some(_)) => std::cmp::Ordering::Less,
            (None, None) => std::cmp::Ordering::Equal,
        }
    }
}

impl std::fmt::Display for HybridLogicalClock {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}:{}", self.physical, self.logical, self.node_id)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hlc_tick() {
        let mut hlc = HybridLogicalClock::new("node1".to_string());
        let t1 = hlc.tick();
        let t2 = hlc.tick();

        assert!(HybridLogicalClock::compare(&t2, &t1) == std::cmp::Ordering::Greater);
    }

    #[test]
    fn test_hlc_parse() {
        let hlc = HybridLogicalClock::new("node1".to_string());
        let s = hlc.to_string();
        let parsed = HybridLogicalClock::parse(&s).unwrap();

        assert_eq!(hlc.physical, parsed.physical);
        assert_eq!(hlc.logical, parsed.logical);
        assert_eq!(hlc.node_id, parsed.node_id);
    }
}