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

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, entry::ID, instance::settings_merge::merge_sync_settings, store::Table,
8    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 (read-only)
66        let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
67        let prefs_db = Database::open_unauthenticated(preferences_db_id.clone(), &instance)?;
68        let current_tips = prefs_db.get_tips().await?;
69
70        // Check if preferences have changed via tip comparison
71        if current_tips == old_tips {
72            debug!(user_uuid = %user_uuid_str, "No changes to user preferences, skipping update");
73            return Ok(());
74        }
75
76        debug!(user_uuid = %user_uuid_str, "User preferences changed, updating sync configuration");
77
78        // Read all tracked databases
79        let databases_table = prefs_db
80            .get_store_viewer::<Table<TrackedDatabase>>("databases")
81            .await?;
82        let all_tracked = databases_table.search(|_| true).await?; // Get all entries
83
84        // Get databases user previously tracked
85        let old_databases = user_mgr.get_linked_databases(user_uuid_str).await?;
86
87        // Build set of current database IDs
88        let current_databases: std::collections::HashSet<_> = all_tracked
89            .iter()
90            .map(|(_uuid, tracked)| tracked)
91            .filter(|t| t.sync_settings.sync_enabled)
92            .map(|t| t.database_id.clone())
93            .collect();
94
95        // Track which databases need settings recomputation
96        let mut affected_databases = std::collections::HashSet::new();
97
98        // Remove user from databases they no longer track
99        for old_db in &old_databases {
100            if !current_databases.contains(old_db) {
101                user_mgr
102                    .unlink_user_from_database(old_db, user_uuid_str)
103                    .await?;
104                affected_databases.insert(old_db.clone());
105                debug!(user_uuid = %user_uuid_str, database_id = %old_db, "Removed user from database");
106            }
107        }
108
109        // Add/update user for current databases
110        for (_uuid, tracked) in &all_tracked {
111            if tracked.sync_settings.sync_enabled {
112                user_mgr
113                    .link_user_to_database(&tracked.database_id, user_uuid_str)
114                    .await?;
115                affected_databases.insert(tracked.database_id.clone());
116            }
117        }
118
119        // Recompute combined settings for all affected databases
120        let affected_count = affected_databases.len();
121        for db_id in affected_databases {
122            let users = user_mgr.get_linked_users(&db_id).await?;
123
124            if users.is_empty() {
125                // No users tracking this database, remove settings
126                continue;
127            }
128
129            // Collect settings from all users tracking this database
130            let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
131            let mut settings_list = Vec::new();
132            for uuid in &users {
133                // Read preferences from each user's database
134                if let Some((user_prefs_db_id, _)) = user_mgr.get_tracked_user_state(uuid).await? {
135                    let user_db = Database::open_unauthenticated(user_prefs_db_id, &instance)?;
136                    let user_table = user_db
137                        .get_store_viewer::<Table<TrackedDatabase>>("databases")
138                        .await?;
139
140                    // Find this database's settings
141                    for (_key, tracked) in user_table.search(|_| true).await? {
142                        if tracked.database_id == db_id && tracked.sync_settings.sync_enabled {
143                            settings_list.push(tracked.sync_settings.clone());
144                            break;
145                        }
146                    }
147                }
148            }
149
150            // Merge settings using most aggressive strategy
151            if !settings_list.is_empty() {
152                let combined = merge_sync_settings(settings_list);
153                user_mgr.set_combined_settings(&db_id, &combined).await?;
154                debug!(database_id = %db_id, "Updated combined settings for database");
155            }
156        }
157
158        // Update stored tips to reflect processed state
159        user_mgr
160            .update_tracked_tips(user_uuid_str, &current_tips)
161            .await?;
162
163        // Commit all changes atomically
164        tx.commit().await?;
165
166        info!(user_uuid = %user_uuid_str, affected_count = affected_count, "Updated user database sync configuration");
167        Ok(())
168    }
169
170    /// Remove a user from the sync system.
171    ///
172    /// Removes all tracking for this user and updates affected databases'
173    /// combined settings. This should be called when a user is deleted.
174    ///
175    /// # Arguments
176    /// * `user_uuid` - The user's unique identifier
177    ///
178    /// # Returns
179    /// A Result indicating success or an error.
180    pub async fn remove_user(&self, user_uuid: impl AsRef<str>) -> Result<()> {
181        let user_uuid_str = user_uuid.as_ref();
182        let tx = self.sync_tree.new_transaction().await?;
183        let user_mgr = UserSyncManager::new(&tx);
184
185        // Get all databases this user was tracking
186        let databases = user_mgr.get_linked_databases(user_uuid_str).await?;
187
188        // Remove user from each database
189        for db_id in &databases {
190            user_mgr
191                .unlink_user_from_database(db_id, user_uuid_str)
192                .await?;
193
194            // Recompute combined settings for this database
195            let remaining_users = user_mgr.get_linked_users(db_id).await?;
196            if remaining_users.is_empty() {
197                // No more users, settings will be cleared automatically
198                continue;
199            }
200
201            // Recompute settings from remaining users
202            // (simplified - in practice would read each user's preferences)
203            // For now, just note that settings need updating
204            debug!(database_id = %db_id, "Database needs settings recomputation after user removal");
205        }
206
207        tx.commit().await?;
208
209        info!(user_uuid = %user_uuid_str, database_count = databases.len(), "Removed user from sync system");
210        Ok(())
211    }
212}