Skip to main content

subcog/services/
sync.rs

1//! Memory synchronization service.
2//!
3//! **Note**: With the migration to `SQLite` as authoritative storage, remote sync
4//! is no longer supported. This service now returns no-op results for all
5//! operations. Git context detection (branch, remote info) remains available
6//! via `RemoteManager` for faceting purposes.
7
8use crate::Result;
9use crate::config::Config;
10use crate::git::RemoteManager;
11use std::time::Instant;
12use tracing::instrument;
13
14/// Service for synchronizing memories with remote storage.
15///
16/// **Deprecated**: With `SQLite` as authoritative storage, remote sync is no longer
17/// supported. All sync operations return empty stats. Use this service only for
18/// remote context detection.
19pub struct SyncService {
20    /// Configuration.
21    config: Config,
22}
23
24impl SyncService {
25    /// Creates a new sync service.
26    #[must_use]
27    pub const fn new(config: Config) -> Self {
28        Self { config }
29    }
30
31    /// Creates a no-op sync service for user-scoped storage.
32    ///
33    /// This service does nothing when sync operations are called,
34    /// returning empty stats.
35    #[must_use]
36    pub fn no_op() -> Self {
37        Self {
38            config: Config::default(),
39        }
40    }
41
42    /// Returns whether this service can perform sync operations.
43    ///
44    /// **Note**: Always returns `false` since remote sync is no longer supported.
45    #[must_use]
46    pub const fn is_enabled(&self) -> bool {
47        false // Remote sync no longer supported
48    }
49
50    /// Fetches memories from remote.
51    ///
52    /// **Note**: Returns empty stats since remote sync is no longer supported.
53    ///
54    /// # Errors
55    ///
56    /// This function does not return errors (always succeeds with empty stats).
57    #[instrument(skip(self), fields(operation = "sync.fetch"))]
58    pub fn fetch(&self) -> Result<SyncStats> {
59        let start = Instant::now();
60
61        // Remote sync no longer supported - return empty stats
62        let stats = SyncStats::default();
63
64        metrics::counter!(
65            "memory_sync_total",
66            "direction" => "fetch",
67            "domain" => "project",
68            "status" => "noop"
69        )
70        .increment(1);
71        metrics::histogram!("memory_sync_duration_ms", "direction" => "fetch")
72            .record(start.elapsed().as_secs_f64() * 1000.0);
73
74        Ok(stats)
75    }
76
77    /// Pushes memories to remote.
78    ///
79    /// **Note**: Returns empty stats since remote sync is no longer supported.
80    ///
81    /// # Errors
82    ///
83    /// This function does not return errors (always succeeds with empty stats).
84    #[instrument(skip(self), fields(operation = "sync.push"))]
85    pub fn push(&self) -> Result<SyncStats> {
86        let start = Instant::now();
87
88        // Remote sync no longer supported - return empty stats
89        let stats = SyncStats::default();
90
91        metrics::counter!(
92            "memory_sync_total",
93            "direction" => "push",
94            "domain" => "project",
95            "status" => "noop"
96        )
97        .increment(1);
98        metrics::histogram!("memory_sync_duration_ms", "direction" => "push")
99            .record(start.elapsed().as_secs_f64() * 1000.0);
100
101        Ok(stats)
102    }
103
104    /// Performs a full sync (fetch + push).
105    ///
106    /// **Note**: Returns empty stats since remote sync is no longer supported.
107    ///
108    /// # Errors
109    ///
110    /// This function does not return errors (always succeeds with empty stats).
111    #[instrument(skip(self), fields(operation = "sync.full"))]
112    pub fn sync(&self) -> Result<SyncStats> {
113        let start = Instant::now();
114
115        // Remote sync no longer supported - return empty stats
116        let stats = SyncStats::default();
117
118        metrics::counter!(
119            "memory_sync_total",
120            "direction" => "full",
121            "domain" => "project",
122            "status" => "noop"
123        )
124        .increment(1);
125        metrics::histogram!("memory_sync_duration_ms", "direction" => "full")
126            .record(start.elapsed().as_secs_f64() * 1000.0);
127
128        Ok(stats)
129    }
130
131    /// Checks if sync is available (remote exists and is reachable).
132    ///
133    /// **Note**: Always returns `false` since remote sync is no longer supported.
134    ///
135    /// # Errors
136    ///
137    /// This function does not return errors (always succeeds with `false`).
138    #[allow(clippy::unused_self)]
139    pub const fn is_available(&self) -> Result<bool> {
140        Ok(false) // Remote sync no longer supported
141    }
142
143    /// Returns the configured remote name.
144    ///
145    /// Can still be used for git context detection.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if no repo is configured.
150    pub fn remote_name(&self) -> Result<Option<String>> {
151        let repo_path = match &self.config.repo_path {
152            Some(p) => p,
153            None => return Ok(None),
154        };
155
156        let remote = RemoteManager::new(repo_path);
157        remote.default_remote()
158    }
159
160    /// Returns the remote URL.
161    ///
162    /// Can still be used for git context detection.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if no repo is configured.
167    pub fn remote_url(&self) -> Result<Option<String>> {
168        let repo_path = match &self.config.repo_path {
169            Some(p) => p,
170            None => return Ok(None),
171        };
172
173        let remote = RemoteManager::new(repo_path);
174        let remote_name = match remote.default_remote()? {
175            Some(name) => name,
176            None => return Ok(None),
177        };
178
179        remote.get_remote_url(&remote_name)
180    }
181}
182
183impl Default for SyncService {
184    fn default() -> Self {
185        Self::new(Config::default())
186    }
187}
188
189/// Statistics from a sync operation.
190#[derive(Debug, Clone, Default)]
191pub struct SyncStats {
192    /// Number of memories pushed.
193    pub pushed: usize,
194    /// Number of memories pulled.
195    pub pulled: usize,
196    /// Number of conflicts encountered.
197    pub conflicts: usize,
198}
199
200impl SyncStats {
201    /// Returns true if the sync was a no-op.
202    #[must_use]
203    pub const fn is_empty(&self) -> bool {
204        self.pushed == 0 && self.pulled == 0 && self.conflicts == 0
205    }
206
207    /// Returns a human-readable summary.
208    #[must_use]
209    pub fn summary(&self) -> String {
210        if self.is_empty() {
211            "Already up to date".to_string()
212        } else {
213            format!(
214                "Pushed: {}, Pulled: {}, Conflicts: {}",
215                self.pushed, self.pulled, self.conflicts
216            )
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_sync_stats_empty() {
227        let stats = SyncStats::default();
228        assert!(stats.is_empty());
229        assert_eq!(stats.summary(), "Already up to date");
230    }
231
232    #[test]
233    fn test_sync_stats_summary() {
234        let stats = SyncStats {
235            pushed: 5,
236            pulled: 3,
237            conflicts: 1,
238        };
239        assert!(!stats.is_empty());
240        assert!(stats.summary().contains("Pushed: 5"));
241        assert!(stats.summary().contains("Pulled: 3"));
242        assert!(stats.summary().contains("Conflicts: 1"));
243    }
244
245    #[test]
246    fn test_sync_service_no_repo() {
247        let service = SyncService::default();
248
249        // All sync operations return Ok with empty stats (no-op)
250        let result = service.fetch();
251        assert!(result.is_ok());
252        assert!(result.unwrap().is_empty());
253
254        let result = service.push();
255        assert!(result.is_ok());
256        assert!(result.unwrap().is_empty());
257
258        let result = service.sync();
259        assert!(result.is_ok());
260        assert!(result.unwrap().is_empty());
261    }
262
263    #[test]
264    fn test_sync_service_availability() {
265        let service = SyncService::default();
266        assert!(!service.is_available().unwrap());
267    }
268}