Skip to main content

bsv_wallet_toolbox/storage/sync/
sync_map.rs

1//! Sync types: SyncChunk, SyncMap, EntitySyncMap.
2//!
3//! These types drive the incremental sync protocol between storage providers.
4//! SyncMap is stored as JSON in the sync_states table for persistent tracking.
5//! SyncChunk is the wire format for sending incremental entity updates.
6
7use std::collections::HashMap;
8
9use chrono::NaiveDateTime;
10use serde::{Deserialize, Serialize};
11
12use crate::tables::*;
13
14/// An incremental chunk of entities that changed since the last sync.
15///
16/// Sent from one storage provider to another during synchronization.
17/// Each entity list is optional -- only populated entity types are included.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SyncChunk {
21    /// Identity key of the source storage.
22    pub from_storage_identity_key: String,
23    /// Identity key of the destination storage.
24    pub to_storage_identity_key: String,
25    /// Identity key of the user being synced.
26    pub user_identity_key: String,
27    /// User record, if changed.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub user: Option<User>,
30    /// Changed proven transaction records.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub proven_txs: Option<Vec<ProvenTx>>,
33    /// Changed output basket records.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub output_baskets: Option<Vec<OutputBasket>>,
36    /// Changed transaction records.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub transactions: Option<Vec<Transaction>>,
39    /// Changed output records.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub outputs: Option<Vec<Output>>,
42    /// Changed transaction label records.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub tx_labels: Option<Vec<TxLabel>>,
45    /// Changed transaction label mapping records.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub tx_label_maps: Option<Vec<TxLabelMap>>,
48    /// Changed output tag records.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub output_tags: Option<Vec<OutputTag>>,
51    /// Changed output tag mapping records.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub output_tag_maps: Option<Vec<OutputTagMap>>,
54    /// Changed certificate records.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub certificates: Option<Vec<Certificate>>,
57    /// Changed certificate field records.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub certificate_fields: Option<Vec<CertificateField>>,
60    /// Changed commission records.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub commissions: Option<Vec<Commission>>,
63    /// Changed proven transaction request records.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub proven_tx_reqs: Option<Vec<ProvenTxReq>>,
66}
67
68/// Per-entity sync tracking: ID mappings, max timestamp, and cumulative count.
69///
70/// `id_map` maps foreign IDs to local IDs so that foreign key references
71/// in dependent entities can be remapped during processSyncChunk.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct EntitySyncMap {
75    /// Name of the entity type (e.g., "provenTx", "transaction").
76    pub entity_name: String,
77    /// Maps foreign entity IDs to local entity IDs.
78    pub id_map: HashMap<i64, i64>,
79    /// The maximum updated_at value seen for this entity over all chunks received.
80    #[serde(default, with = "crate::serde_datetime::option")]
81    pub max_updated_at: Option<NaiveDateTime>,
82    /// Cumulative count of items received since the last `since` update.
83    pub count: i64,
84}
85
86impl EntitySyncMap {
87    /// Create a new EntitySyncMap for the given entity name.
88    pub fn new(entity_name: &str) -> Self {
89        Self {
90            entity_name: entity_name.to_string(),
91            id_map: HashMap::new(),
92            max_updated_at: None,
93            count: 0,
94        }
95    }
96
97    /// Update the max_updated_at to the greater of current and incoming.
98    pub fn update_max(&mut self, updated_at: NaiveDateTime) {
99        match self.max_updated_at {
100            Some(current) if current >= updated_at => {}
101            _ => {
102                self.max_updated_at = Some(updated_at);
103            }
104        }
105    }
106
107    /// Record a foreign-to-local ID mapping. Returns an error if a conflicting
108    /// mapping already exists.
109    pub fn map_id(&mut self, foreign_id: i64, local_id: i64) -> Result<(), String> {
110        if let Some(existing) = self.id_map.get(&foreign_id) {
111            if *existing != local_id {
112                return Err(format!(
113                    "EntitySyncMap[{}]: cannot override mapping {}=>{} with {}",
114                    self.entity_name, foreign_id, existing, local_id
115                ));
116            }
117        }
118        self.id_map.insert(foreign_id, local_id);
119        Ok(())
120    }
121
122    /// Look up the local ID for a foreign ID. Returns None if not mapped.
123    pub fn get_local_id(&self, foreign_id: i64) -> Option<i64> {
124        self.id_map.get(&foreign_id).copied()
125    }
126}
127
128/// Tracks per-entity sync state for all syncable entity types.
129///
130/// One SyncMap exists per storage-pair sync relationship. It is serialized
131/// to JSON and stored in the sync_states table.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct SyncMap {
135    /// Sync state for proven transactions.
136    pub proven_tx: EntitySyncMap,
137    /// Sync state for output baskets.
138    pub output_basket: EntitySyncMap,
139    /// Sync state for transactions.
140    pub transaction: EntitySyncMap,
141    /// Sync state for outputs.
142    pub output: EntitySyncMap,
143    /// Sync state for transaction labels.
144    pub tx_label: EntitySyncMap,
145    /// Sync state for transaction label mappings.
146    pub tx_label_map: EntitySyncMap,
147    /// Sync state for output tags.
148    pub output_tag: EntitySyncMap,
149    /// Sync state for output tag mappings.
150    pub output_tag_map: EntitySyncMap,
151    /// Sync state for certificates.
152    pub certificate: EntitySyncMap,
153    /// Sync state for certificate fields.
154    pub certificate_field: EntitySyncMap,
155    /// Sync state for commissions.
156    pub commission: EntitySyncMap,
157    /// Sync state for proven transaction requests.
158    pub proven_tx_req: EntitySyncMap,
159}
160
161impl SyncMap {
162    /// Create a new SyncMap with all EntitySyncMaps initialized to empty state.
163    pub fn new() -> Self {
164        Self {
165            proven_tx: EntitySyncMap::new("provenTx"),
166            output_basket: EntitySyncMap::new("outputBasket"),
167            transaction: EntitySyncMap::new("transaction"),
168            output: EntitySyncMap::new("output"),
169            tx_label: EntitySyncMap::new("txLabel"),
170            tx_label_map: EntitySyncMap::new("txLabelMap"),
171            output_tag: EntitySyncMap::new("outputTag"),
172            output_tag_map: EntitySyncMap::new("outputTagMap"),
173            certificate: EntitySyncMap::new("certificate"),
174            certificate_field: EntitySyncMap::new("certificateField"),
175            commission: EntitySyncMap::new("commission"),
176            proven_tx_req: EntitySyncMap::new("provenTxReq"),
177        }
178    }
179}
180
181impl Default for SyncMap {
182    fn default() -> Self {
183        Self::new()
184    }
185}