Skip to main content

meshlet_core/
lib.rs

1pub mod error;
2pub mod model;
3pub mod doc;
4pub mod store;
5pub mod search;
6pub mod fetch;
7pub mod reconcile;
8
9pub use rusqlite;
10pub use loro;
11
12use std::path::Path;
13
14use crate::error::MeshletError;
15use error::Result;
16use model::{Bookmark, BookmarkId, BookmarkPatch};
17use store::Store;
18
19#[derive(Debug, Clone, Default)]
20pub struct SyncSummary {
21    pub merged_duplicates: usize,
22}
23
24pub struct MeshletDb {
25    inner: doc::LoroStore,
26    db: Store,
27}
28
29impl MeshletDb {
30    pub fn open(path: &Path) -> Result<Self> {
31        let db = Store::open(path)?;
32
33        let inner = if let Some(snapshot) = db.load_snapshot()? {
34            doc::LoroStore::from_snapshot(&snapshot)?
35        } else {
36            let peer_id = ulid::Ulid::new().to_string();
37            db.set_meta_string("peer_id", &peer_id)?;
38            doc::LoroStore::new()
39        };
40
41        let this = Self { inner, db };
42        this.rebuild_mirror()?;
43
44        Ok(this)
45    }
46
47    pub fn open_in_memory() -> Result<Self> {
48        let db = Store::open_in_memory()?;
49        let inner = doc::LoroStore::new();
50        Ok(Self { inner, db })
51    }
52
53    pub fn add_bookmark(&self, b: &Bookmark) -> Result<()> {
54        self.inner.add_bookmark(b)?;
55        self.db.upsert_bookmark(b)?;
56        self.save_snapshot()?;
57        Ok(())
58    }
59
60    pub fn update_bookmark(&self, id: &BookmarkId, patch: &BookmarkPatch) -> Result<()> {
61        self.inner.update_bookmark(id, patch)?;
62        if let Some(updated) = self.inner.get_bookmark(id) {
63            self.db.upsert_bookmark(&updated)?;
64        }
65        self.save_snapshot()?;
66        Ok(())
67    }
68
69    pub fn delete_bookmark(&self, id: &BookmarkId) -> Result<()> {
70        self.inner.delete_bookmark(id)?;
71        self.db.delete_bookmark_mirror(id.as_str())?;
72        self.save_snapshot()?;
73        Ok(())
74    }
75
76    pub fn add_tags(&self, id: &BookmarkId, tags: &[String]) -> Result<()> {
77        self.inner.add_tags(id, tags)?;
78        if let Some(updated) = self.inner.get_bookmark(id) {
79            self.db.upsert_bookmark(&updated)?;
80        }
81        self.save_snapshot()?;
82        Ok(())
83    }
84
85    pub fn remove_tags(&self, id: &BookmarkId, tags: &[String]) -> Result<()> {
86        self.inner.remove_tags(id, tags)?;
87        if let Some(updated) = self.inner.get_bookmark(id) {
88            self.db.upsert_bookmark(&updated)?;
89        }
90        self.save_snapshot()?;
91        Ok(())
92    }
93
94    pub fn get_bookmark(&self, id: &BookmarkId) -> Option<Bookmark> {
95        self.inner.get_bookmark(id)
96    }
97
98    pub fn list_bookmarks(&self) -> Vec<Bookmark> {
99        self.inner.list_bookmarks()
100    }
101
102    pub fn import(&self, data: &[u8]) -> Result<()> {
103        self.inner.import(data)?;
104        reconcile::reconcile(&self.inner)?;
105        self.rebuild_mirror()?;
106        self.save_snapshot()?;
107        Ok(())
108    }
109
110    pub fn export_snapshot(&self) -> Result<Vec<u8>> {
111        self.inner.export_snapshot()
112    }
113
114    pub fn search_keywords(
115        &self,
116        keywords: &[String],
117        deep: bool,
118        all_match: bool,
119    ) -> Result<Vec<Bookmark>> {
120        search::search_keywords(self.db.connection(), keywords, deep, all_match)
121    }
122
123    pub fn search_by_tags(&self, tags: &[String]) -> Result<Vec<Bookmark>> {
124        search::search_by_tags(self.db.connection(), tags)
125    }
126
127    pub fn list_from_mirror(&self) -> Result<Vec<Bookmark>> {
128        search::list_all(self.db.connection())
129    }
130
131    pub fn inner_connection(&self) -> &rusqlite::Connection {
132        self.db.connection()
133    }
134
135    pub fn oplog_vv(&self) -> loro::VersionVector {
136        self.inner.oplog_vv()
137    }
138
139    pub fn export_updates_since(&self, vv: &loro::VersionVector) -> Result<Vec<u8>> {
140        self.inner.export_updates_since(vv)
141    }
142
143    pub fn sync_import(&self, data: &[u8]) -> Result<SyncSummary> {
144        self.inner.import(data)?;
145        let merged = reconcile::reconcile(&self.inner)?;
146        self.rebuild_mirror()?;
147        self.save_snapshot()?;
148        Ok(SyncSummary {
149            merged_duplicates: merged,
150        })
151    }
152
153    pub fn compact_change_store(&self) {
154        self.inner.compact_change_store();
155    }
156
157    pub fn save_last_server_vv(&self, vv: &loro::VersionVector) -> Result<()> {
158        let data = serde_json::to_vec(vv)
159            .map_err(|e| MeshletError::SerializationError(e.to_string()))?;
160        self.db.set_meta("last_server_vv", &data)
161    }
162
163    pub fn load_last_server_vv(&self) -> Result<Option<loro::VersionVector>> {
164        match self.db.get_meta("last_server_vv")? {
165            Some(data) => Ok(Some(serde_json::from_slice(&data)
166                .map_err(|e| MeshletError::SerializationError(e.to_string()))?)),
167            None => Ok(None),
168        }
169    }
170
171    fn rebuild_mirror(&self) -> Result<()> {
172        self.db.connection().execute("DELETE FROM bookmark_tags", [])?;
173        self.db.connection().execute("DELETE FROM bookmarks", [])?;
174        let bookmarks = self.inner.list_bookmarks();
175        for b in &bookmarks {
176            self.db.upsert_bookmark(b)?;
177        }
178        Ok(())
179    }
180    fn save_snapshot(&self) -> Result<()> {
181        let snapshot = self.inner.export_snapshot()?;
182        let vv = serde_json::to_vec(&self.inner.oplog_vv())
183            .map_err(|e| error::MeshletError::SerializationError(e.to_string()))?;
184        self.db.save_snapshot(&snapshot, &vv)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_mirror_rebuilt_on_restart() {
194        let dir = tempfile::tempdir().unwrap();
195        let path = dir.path().join("test.db");
196
197        let b = Bookmark {
198            id: BookmarkId::new(),
199            url: "https://example.com".into(),
200            title: "Example".into(),
201            desc: "A test bookmark".into(),
202            tags: {
203                let mut t = std::collections::BTreeSet::new();
204                t.insert("test".into());
205                t.insert("example".into());
206                t
207            },
208            flags: 0,
209            created_at: 0,
210            updated_at: 0,
211        };
212
213        {
214            let db = MeshletDb::open(&path).unwrap();
215            db.add_bookmark(&b).unwrap();
216        }
217
218        {
219            let db = MeshletDb::open(&path).unwrap();
220            let loaded = db.get_bookmark(&b.id).unwrap();
221            assert_eq!(loaded.url, "https://example.com");
222            assert_eq!(loaded.title, "Example");
223            assert_eq!(loaded.desc, "A test bookmark");
224            assert!(loaded.tags.contains("test"));
225            assert!(loaded.tags.contains("example"));
226        }
227    }
228
229    #[test]
230    fn test_search_from_mirror() {
231        let db = MeshletDb::open_in_memory().unwrap();
232
233        let b1 = Bookmark {
234            id: BookmarkId::new(),
235            url: "https://rust-lang.org".into(),
236            title: "Rust Programming Language".into(),
237            desc: "A systems programming language".into(),
238            tags: {
239                let mut t = std::collections::BTreeSet::new();
240                t.insert("rust".into());
241                t
242            },
243            flags: 0,
244            created_at: 0,
245            updated_at: 0,
246        };
247
248        let b2 = Bookmark {
249            id: BookmarkId::new(),
250            url: "https://loro.dev".into(),
251            title: "Loro CRDT".into(),
252            desc: "CRDT framework".into(),
253            tags: {
254                let mut t = std::collections::BTreeSet::new();
255                t.insert("crdt".into());
256                t.insert("rust".into());
257                t
258            },
259            flags: 0,
260            created_at: 0,
261            updated_at: 0,
262        };
263
264        db.add_bookmark(&b1).unwrap();
265        db.add_bookmark(&b2).unwrap();
266
267        let results = db.search_keywords(&["crdt".into()], false, false).unwrap();
268        assert_eq!(results.len(), 1);
269        assert_eq!(results[0].url, "https://loro.dev");
270
271        let results = db.search_keywords(&["rust".into()], false, false).unwrap();
272        assert_eq!(results.len(), 1);
273        assert_eq!(results[0].url, "https://rust-lang.org");
274
275        let results = db.search_by_tags(&["crdt".into()]).unwrap();
276        assert_eq!(results.len(), 1);
277        assert_eq!(results[0].url, "https://loro.dev");
278    }
279
280    #[test]
281    fn test_delete_persists_across_restart() {
282        let dir = tempfile::tempdir().unwrap();
283        let path = dir.path().join("test.db");
284
285        let b = Bookmark {
286            id: BookmarkId::new(),
287            url: "https://example.com".into(),
288            title: "Example".into(),
289            desc: "".into(),
290            tags: std::collections::BTreeSet::new(),
291            flags: 0,
292            created_at: 0,
293            updated_at: 0,
294        };
295
296        {
297            let db = MeshletDb::open(&path).unwrap();
298            db.add_bookmark(&b).unwrap();
299            assert!(db.get_bookmark(&b.id).is_some());
300            db.delete_bookmark(&b.id).unwrap();
301            assert!(db.get_bookmark(&b.id).is_none());
302        }
303
304        {
305            let db = MeshletDb::open(&path).unwrap();
306            assert!(db.get_bookmark(&b.id).is_none());
307            assert_eq!(db.list_bookmarks().len(), 0);
308        }
309    }
310
311    #[test]
312    fn test_update_reflected_in_mirror() {
313        let db = MeshletDb::open_in_memory().unwrap();
314
315        let b = Bookmark {
316            id: BookmarkId::new(),
317            url: "https://old.example.com".into(),
318            title: "Old Title".into(),
319            desc: "Old desc".into(),
320            tags: {
321                let mut t = std::collections::BTreeSet::new();
322                t.insert("initial".into());
323                t
324            },
325            flags: 0,
326            created_at: 0,
327            updated_at: 0,
328        };
329        db.add_bookmark(&b).unwrap();
330
331        let patch = BookmarkPatch {
332            url: Some("https://new.example.com".into()),
333            title: Some("New Title".into()),
334            desc: None,
335            flags: None,
336        };
337        db.update_bookmark(&b.id, &patch).unwrap();
338
339        let mirror_results = db
340            .search_keywords(&["New Title".into()], false, false)
341            .unwrap();
342        assert_eq!(mirror_results.len(), 1);
343        assert_eq!(mirror_results[0].url, "https://new.example.com");
344        assert_eq!(mirror_results[0].title, "New Title");
345        assert_eq!(mirror_results[0].desc, "Old desc");
346    }
347
348    #[test]
349    fn test_tag_sync_in_mirror() {
350        let db = MeshletDb::open_in_memory().unwrap();
351
352        let b = Bookmark {
353            id: BookmarkId::new(),
354            url: "https://example.com".into(),
355            title: "Example".into(),
356            desc: "".into(),
357            tags: std::collections::BTreeSet::new(),
358            flags: 0,
359            created_at: 0,
360            updated_at: 0,
361        };
362        db.add_bookmark(&b).unwrap();
363
364        db.add_tags(&b.id, &["rust".into(), "crdt".into()])
365            .unwrap();
366        db.remove_tags(&b.id, &["crdt".into()]).unwrap();
367
368        let results = db.search_by_tags(&["rust".into()]).unwrap();
369        assert_eq!(results.len(), 1);
370        assert!(results[0].tags.contains("rust"));
371        assert!(!results[0].tags.contains("crdt"));
372    }
373}