Skip to main content

xenith_core/
conflict.rs

1use crate::{ChainId, Result, StateKey, StateValue, XenithError};
2use async_trait::async_trait;
3
4/// Resolves a set of conflicting [`StateValue`]s into a single authoritative value.
5///
6/// Implementations must be `Send + Sync`. The caller is always responsible for
7/// choosing a resolver — xenith never resolves `Diverged` state automatically.
8///
9/// # Example
10///
11/// ```rust,no_run
12/// use xenith_core::{ConflictResolver, LatestVersionResolver, StateKey};
13/// use std::sync::Arc;
14///
15/// # async fn example(candidates: Vec<(xenith_core::ChainId, xenith_core::StateValue)>) {
16/// let resolver = LatestVersionResolver;
17/// let key = StateKey::new("proto", "pool", "0xabc");
18/// let resolved = resolver.resolve(&key, candidates).await.unwrap();
19/// # }
20/// ```
21#[async_trait]
22pub trait ConflictResolver: Send + Sync {
23    /// Reduce `candidates` (one value per chain) to a single resolved [`StateValue`].
24    async fn resolve(
25        &self,
26        key: &StateKey,
27        candidates: Vec<(ChainId, StateValue)>,
28    ) -> Result<StateValue>;
29}
30
31/// Picks the candidate with the highest `version` field.
32///
33/// If two candidates share the same version the one with the lower chain ID wins,
34/// giving a deterministic tie-break.
35pub struct LatestVersionResolver;
36
37#[async_trait]
38impl ConflictResolver for LatestVersionResolver {
39    async fn resolve(
40        &self,
41        key: &StateKey,
42        candidates: Vec<(ChainId, StateValue)>,
43    ) -> Result<StateValue> {
44        candidates
45            .into_iter()
46            .map(|(_, v)| v)
47            .max_by_key(|v| v.version)
48            .ok_or_else(|| XenithError::StoreError(format!("no candidates for key {key}")))
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use bytes::Bytes;
56
57    fn val(chain: u64, ts: u64) -> (ChainId, StateValue) {
58        use crate::StateVersion;
59        (
60            ChainId(chain),
61            StateValue {
62                data: Bytes::from_static(b"d"),
63                version: StateVersion {
64                    timestamp_ms: ts,
65                    sequence: 0,
66                    source_chain: chain,
67                },
68                updated_at: 0,
69                source_chain: ChainId(chain),
70            },
71        )
72    }
73
74    #[tokio::test]
75    async fn picks_highest_version() {
76        use crate::StateVersion;
77        let key = StateKey::new("p", "e", "1");
78        let result = LatestVersionResolver
79            .resolve(&key, vec![val(1, 3), val(42161, 7), val(10, 2)])
80            .await
81            .unwrap();
82        assert_eq!(
83            result.version,
84            StateVersion {
85                timestamp_ms: 7,
86                sequence: 0,
87                source_chain: 42161
88            }
89        );
90    }
91
92    #[tokio::test]
93    async fn empty_candidates_returns_error() {
94        let key = StateKey::new("p", "e", "1");
95        assert!(LatestVersionResolver.resolve(&key, vec![]).await.is_err());
96    }
97}