Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/sync/
user.rs

1//! User synchronization methods for the sync system.
2
3use tracing::{debug, info};
4
5use super::{Sync, SyncError, user_sync_manager::UserSyncManager};
6use crate::{
7    Database, Result, Snapshot, entry::ID, instance::settings_merge::merge_sync_settings,
8    store::Table, user::types::TrackedDatabase,
9};
10
11impl Sync {
12    // === User Synchronization Methods ===
13
14    /// Synchronize a user's preferences with the sync system.
15    ///
16    /// This establishes tracking for a user's preferences database and synchronizes
17    /// their current preferences to the sync tree. The sync system will monitor the
18    /// user's preferences and automatically sync databases according to their settings.
19    ///
20    /// This method ensures the user is tracked and reads their preferences database
21    /// to update sync configuration. It detects changes via tip comparison and only
22    /// processes updates when preferences have changed.
23    ///
24    /// This operation is idempotent and can be called multiple times safely.
25    ///
26    /// **CRITICAL**: All updates to the sync tree happen in a single transaction
27    /// to ensure atomicity.
28    ///
29    /// # Arguments
30    /// * `user_uuid` - The user's unique identifier
31    /// * `preferences_db_id` - The ID of the user's private database
32    ///
33    /// # Returns
34    /// A Result indicating success or an error.
35    ///
36    /// # Example
37    /// ```rust,ignore
38    /// // After creating or logging in a user
39    /// let user = instance.login_user("alice", Some("password"))?;
40    /// sync.sync_user(user.user_uuid(), user.user_database().root_id())?;
41    /// ```
42    pub async fn sync_user(
43        &self,
44        user_uuid: impl AsRef<str>,
45        preferences_db_id: &ID,
46    ) -> Result<()> {
47        let user_uuid_str = user_uuid.as_ref();
48
49        // CRITICAL: Single transaction for all sync tree updates
50        let tx = self.sync_tree.new_transaction().await?;
51        let user_mgr = UserSyncManager::new(&tx);
52
53        // Ensure user is tracked, get their current preferences state
54        let old_tips = match user_mgr.get_tracked_user_state(user_uuid_str).await? {
55            Some((_stored_prefs_db_id, tips)) => tips,
56            None => {
57                // User not yet tracked - register them
58                user_mgr
59                    .track_user_preferences(user_uuid_str, preferences_db_id)
60                    .await?;
61                Vec::new() // Empty tips means this is first sync
62            }
63        };
64
65        // Open user's preferences database with device key (Read permission)
66        let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
67        let device_key = instance.signing_key()?.clone();
68        let prefs_db = Database::open(&instance, preferences_db_id)
69            .await?
70            .with_key(device_key.clone());
71        let current_snapshot = prefs_db.snapshot().await?;
72
73        // Check if preferences have changed via snapshot comparison (set-equal).
74        if current_snapshot == Snapshot::from(old_tips.clone()) {
75            debug!(user_uuid = %user_uuid_str, "No changes to user preferences, skipping update");
76            return Ok(());
77        }
78
79        debug!(user_uuid = %user_uuid_str, "User preferences changed, updating sync configuration");
80
81        // Read all tracked databases
82        let databases_table = prefs_db
83            .get_store_viewer::<Table<TrackedDatabase>>("databases")
84            .await?;
85        let all_tracked = databases_table.search(|_| true).await?; // Get all entries
86
87        // Get databases user previously tracked
88        let old_databases = user_mgr.get_linked_databases(user_uuid_str).await?;
89
90        // Build set of current database IDs
91        let current_databases: std::collections::HashSet<_> = all_tracked
92            .iter()
93            .map(|(_uuid, tracked)| tracked)
94            .filter(|t| t.sync_settings.sync_enabled)
95            .map(|t| t.database_id.clone())
96            .collect();
97
98        // Track which databases need settings recomputation
99        let mut affected_databases = std::collections::HashSet::new();
100
101        // Remove user from databases they no longer track
102        for old_db in &old_databases {
103            if !current_databases.contains(old_db) {
104                user_mgr
105                    .unlink_user_from_database(old_db, user_uuid_str)
106                    .await?;
107                affected_databases.insert(old_db.clone());
108                debug!(user_uuid = %user_uuid_str, database_id = %old_db, "Removed user from database");
109            }
110        }
111
112        // Add/update user for current databases
113        for (_uuid, tracked) in &all_tracked {
114            if tracked.sync_settings.sync_enabled {
115                user_mgr
116                    .link_user_to_database(&tracked.database_id, user_uuid_str)
117                    .await?;
118                affected_databases.insert(tracked.database_id.clone());
119            }
120        }
121
122        // Recompute combined settings for all affected databases
123        let affected_count = affected_databases.len();
124        for db_id in affected_databases {
125            let users = user_mgr.get_linked_users(&db_id).await?;
126
127            if users.is_empty() {
128                // No users tracking this database, remove settings
129                continue;
130            }
131
132            // Collect settings from all users tracking this database
133            let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
134            let device_key = instance.signing_key()?.clone();
135            let mut settings_list = Vec::new();
136            for uuid in &users {
137                // Read preferences from each user's database using device key (Read permission)
138                if let Some((user_prefs_db_id, _)) = user_mgr.get_tracked_user_state(uuid).await? {
139                    let user_db = Database::open(&instance, &user_prefs_db_id)
140                        .await?
141                        .with_key(device_key.clone());
142                    let user_table = user_db
143                        .get_store_viewer::<Table<TrackedDatabase>>("databases")
144                        .await?;
145
146                    // Find this database's settings
147                    for (_key, tracked) in user_table.search(|_| true).await? {
148                        if tracked.database_id == db_id && tracked.sync_settings.sync_enabled {
149                            settings_list.push(tracked.sync_settings.clone());
150                            break;
151                        }
152                    }
153                }
154            }
155
156            // Merge settings using most aggressive strategy
157            if !settings_list.is_empty() {
158                let combined = merge_sync_settings(settings_list);
159                user_mgr.set_combined_settings(&db_id, &combined).await?;
160                debug!(database_id = %db_id, "Updated combined settings for database");
161            }
162        }
163
164        // Update stored tips to reflect processed state
165        user_mgr
166            .update_tracked_tips(user_uuid_str, current_snapshot.tips())
167            .await?;
168
169        // Commit all changes atomically
170        tx.commit().await?;
171
172        info!(user_uuid = %user_uuid_str, affected_count = affected_count, "Updated user database sync configuration");
173        Ok(())
174    }
175
176    /// Remove a user from the sync system.
177    ///
178    /// Removes all tracking for this user and updates affected databases'
179    /// combined settings. This should be called when a user is deleted.
180    ///
181    /// # Arguments
182    /// * `user_uuid` - The user's unique identifier
183    ///
184    /// # Returns
185    /// A Result indicating success or an error.
186    pub async fn remove_user(&self, user_uuid: impl AsRef<str>) -> Result<()> {
187        let user_uuid_str = user_uuid.as_ref();
188        let tx = self.sync_tree.new_transaction().await?;
189        let user_mgr = UserSyncManager::new(&tx);
190
191        // Get all databases this user was tracking
192        let databases = user_mgr.get_linked_databases(user_uuid_str).await?;
193
194        // Remove user from each database
195        for db_id in &databases {
196            user_mgr
197                .unlink_user_from_database(db_id, user_uuid_str)
198                .await?;
199
200            // Recompute combined settings for this database
201            let remaining_users = user_mgr.get_linked_users(db_id).await?;
202            if remaining_users.is_empty() {
203                // No more users, settings will be cleared automatically
204                continue;
205            }
206
207            // Recompute settings from remaining users
208            // (simplified - in practice would read each user's preferences)
209            // For now, just note that settings need updating
210            debug!(database_id = %db_id, "Database needs settings recomputation after user removal");
211        }
212
213        tx.commit().await?;
214
215        info!(user_uuid = %user_uuid_str, database_count = databases.len(), "Removed user from sync system");
216        Ok(())
217    }
218}