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                // unlink_user_from_database removes the whole record, including
129                // stale combined settings, when the last user leaves.
130                continue;
131            }
132
133            // Collect settings from all users tracking this database
134            let instance = self.instance.upgrade().ok_or(SyncError::InstanceDropped)?;
135            let device_key = instance.signing_key()?.clone();
136            let mut settings_list = Vec::new();
137            for uuid in &users {
138                // Read preferences from each user's database using device key (Read permission)
139                if let Some((user_prefs_db_id, _)) = user_mgr.get_tracked_user_state(uuid).await? {
140                    let user_db = Database::open(&instance, &user_prefs_db_id)
141                        .await?
142                        .with_key(device_key.clone());
143                    let user_table = user_db
144                        .get_store_viewer::<Table<TrackedDatabase>>("databases")
145                        .await?;
146
147                    // Find this database's settings
148                    for (_key, tracked) in user_table.search(|_| true).await? {
149                        if tracked.database_id == db_id && tracked.sync_settings.sync_enabled {
150                            settings_list.push(tracked.sync_settings.clone());
151                            break;
152                        }
153                    }
154                }
155            }
156
157            // Merge settings using most aggressive strategy
158            if !settings_list.is_empty() {
159                let combined = merge_sync_settings(settings_list);
160                user_mgr.set_combined_settings(&db_id, &combined).await?;
161                debug!(database_id = %db_id, "Updated combined settings for database");
162            }
163        }
164
165        // Update stored tips to reflect processed state
166        user_mgr
167            .update_tracked_tips(user_uuid_str, current_snapshot.tips())
168            .await?;
169
170        // Commit all changes atomically
171        tx.commit().await?;
172
173        info!(user_uuid = %user_uuid_str, affected_count = affected_count, "Updated user database sync configuration");
174        Ok(())
175    }
176
177    /// Remove a user from the sync system.
178    ///
179    /// Removes all tracking for this user and updates affected databases'
180    /// combined settings. This should be called when a user is deleted.
181    ///
182    /// # Arguments
183    /// * `user_uuid` - The user's unique identifier
184    ///
185    /// # Returns
186    /// A Result indicating success or an error.
187    pub async fn remove_user(&self, user_uuid: impl AsRef<str>) -> Result<()> {
188        let user_uuid_str = user_uuid.as_ref();
189        let tx = self.sync_tree.new_transaction().await?;
190        let user_mgr = UserSyncManager::new(&tx);
191
192        // Get all databases this user was tracking
193        let databases = user_mgr.get_linked_databases(user_uuid_str).await?;
194
195        // Remove user from each database
196        for db_id in &databases {
197            user_mgr
198                .unlink_user_from_database(db_id, user_uuid_str)
199                .await?;
200
201            // Recompute combined settings for this database
202            let remaining_users = user_mgr.get_linked_users(db_id).await?;
203            if remaining_users.is_empty() {
204                // No more users, settings will be cleared automatically
205                continue;
206            }
207
208            // Recompute settings from remaining users
209            // (simplified - in practice would read each user's preferences)
210            // For now, just note that settings need updating
211            debug!(database_id = %db_id, "Database needs settings recomputation after user removal");
212        }
213
214        tx.commit().await?;
215
216        info!(user_uuid = %user_uuid_str, database_count = databases.len(), "Removed user from sync system");
217        Ok(())
218    }
219}