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, ¤t_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}