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}