// LinkSync Sync Engine // Handles all three sync modes with conflict resolution const SyncEngine = { MODES: { BIDIRECTIONAL: "bi-directional", BROWSER_AUTHORITY: "browser-authoritative", SERVER_AUTHORITY: "server-authoritative", }, async getConfig() { const result = await browser.storage.local.get([ "linksync_sync_mode", "linksync_deletions", "linksync_collection", ]); return { mode: result.linksync_sync_mode || this.MODES.BIDIRECTIONAL, deletions: result.linksync_deletions === true, collection: result.linksync_collection || null, }; }, async runSync() { const config = await this.getConfig(); const browserBookmarks = await this.getBrowserBookmarks(); const serverBookmarks = await API.getLinks({ limit: 1000 }); let actions = []; switch (config.mode) { case this.MODES.BIDIRECTIONAL: actions = await this.bidirectionalSync(browserBookmarks, serverBookmarks, config); break; case this.MODES.BROWSER_AUTHORITY: actions = await this.browserAuthoritativeSync(browserBookmarks, serverBookmarks, config); break; case this.MODES.SERVER_AUTHORITY: actions = await this.serverAuthoritativeSync(browserBookmarks, serverBookmarks, config); break; } await browser.storage.local.set({ linksync_last_sync: new Date().toISOString(), }); return actions; }, async getBrowserBookmarks() { const tree = await browser.bookmarks.getTree(); const flat = []; this.flattenBookmarks(tree, flat); return flat; }, flattenBookmarks(nodes, result, path = "") { for (const node of nodes) { if (node.url) { result.push({ id: node.id, url: node.url, title: node.title || "", dateAdded: node.dateAdded ? new Date(node.dateAdded).toISOString() : null, path: path, }); } if (node.children && node.children.length > 0) { const newPath = node.title ? `${path}/${node.title}` : path; this.flattenBookmarks(node.children, result, newPath); } } }, async bidirectionalSync(browserBookmarks, serverBookmarks, config) { const actions = []; const serverByUrl = {}; const browserByUrl = {}; for (const bm of serverBookmarks || []) { serverByUrl[bm.url.toLowerCase()] = bm; } for (const bm of browserBookmarks) { browserByUrl[bm.url.toLowerCase()] = bm; } // Push new/updated browser bookmarks to server for (const bm of browserBookmarks) { const key = bm.url.toLowerCase(); const serverBm = serverByUrl[key]; if (!serverBm) { try { const created = await API.createLink({ url: bm.url, title: bm.title, tags: [], path: bm.path, }); actions.push({ type: "create", url: bm.url, target: "server", id: created?.id }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "server", error: e.message }); } } } // Push new server bookmarks to browser for (const bm of serverBookmarks || []) { const key = bm.url.toLowerCase(); const browserBm = browserByUrl[key]; if (!browserBm) { try { const created = await browser.bookmarks.create({ url: bm.url, title: bm.title, }); actions.push({ type: "create", url: bm.url, target: "browser", id: created?.id }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "browser", error: e.message }); } } } // Handle deletions if (config.deletions) { for (const key in serverByUrl) { if (!browserByUrl[key]) { try { await API.deleteLink(serverByUrl[key].id); actions.push({ type: "delete", url: key, target: "server" }); } catch (e) { actions.push({ type: "error", url: key, target: "server", error: e.message }); } } } } return actions; }, async browserAuthoritativeSync(browserBookmarks, serverBookmarks, config) { const actions = []; const serverByUrl = {}; for (const bm of serverBookmarks || []) { serverByUrl[bm.url.toLowerCase()] = bm; } // Push all browser bookmarks to server (overwriting if exists) for (const bm of browserBookmarks) { const key = bm.url.toLowerCase(); const serverBm = serverByUrl[key]; if (!serverBm) { try { const created = await API.createLink({ url: bm.url, title: bm.title, tags: [], path: bm.path, }); actions.push({ type: "create", url: bm.url, target: "server", id: created?.id }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "server", error: e.message }); } } else { try { await API.updateLink(serverBm.id, { url: bm.url, title: bm.title, }); actions.push({ type: "update", url: bm.url, target: "server" }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "server", error: e.message }); } } } // Delete server bookmarks not in browser if (config.deletions) { const browserUrls = new Set(browserBookmarks.map((b) => b.url.toLowerCase())); for (const key in serverByUrl) { if (!browserUrls.has(key)) { try { await API.deleteLink(serverByUrl[key].id); actions.push({ type: "delete", url: key, target: "server" }); } catch (e) { actions.push({ type: "error", url: key, target: "server", error: e.message }); } } } } return actions; }, async serverAuthoritativeSync(browserBookmarks, serverBookmarks, config) { const actions = []; const browserByUrl = {}; for (const bm of browserBookmarks) { browserByUrl[bm.url.toLowerCase()] = bm; } // Download all server bookmarks to browser for (const bm of serverBookmarks || []) { const key = bm.url.toLowerCase(); const browserBm = browserByUrl[key]; if (!browserBm) { try { const created = await browser.bookmarks.create({ url: bm.url, title: bm.title, }); actions.push({ type: "create", url: bm.url, target: "browser", id: created?.id }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "browser", error: e.message }); } } else { // Overwrite local on conflict try { await browser.bookmarks.update(browserBm.id, { title: bm.title, }); actions.push({ type: "update", url: bm.url, target: "browser" }); } catch (e) { actions.push({ type: "error", url: bm.url, target: "browser", error: e.message }); } } } return actions; }, detectConflicts(browserBookmarks, serverBookmarks) { const conflicts = []; const serverByUrl = {}; for (const bm of serverBookmarks || []) { serverByUrl[bm.url.toLowerCase()] = bm; } for (const bm of browserBookmarks) { const key = bm.url.toLowerCase(); const serverBm = serverByUrl[key]; if (serverBm && serverBm.title !== bm.title) { conflicts.push({ url: bm.url, browserTitle: bm.title, serverTitle: serverBm.title, browserId: bm.id, serverId: serverBm.id, }); } } return conflicts; }, };