/** * GENERAL PROCESS: * make a get request to server with current lastSyncedTimestamp * parse the response. the response should contain everything that user has on the server * decrypt the response * merge everything into the database and look for conflicts * send the conflicts (if any) to the end-user for resolution * once the conflicts have been resolved, send the updated data back to the server */ /** * MERGING: * Locally, get everything that was editted/added after the lastSyncedTimestamp * Run forEach loop on the server response. * Add items that do not exist in the local collections * Remove items (without asking) that need to be removed * Update items that were editted before the lastSyncedTimestamp * Try to merge items that were edited after the lastSyncedTimestamp * Items in which the content has changed, send them for conflict resolution * Otherwise, keep the most recently updated copy. */ /** * CONFLICTS: * Syncing should pause until all the conflicts have been resolved * And then it should continue. */ import Database from "../index"; import { HOST, HEADERS } from "../../utils/constants"; import Prepare from "./prepare"; import Merger from "./merger"; import { areAllEmpty } from "./utils"; var tfun = require("transfun/transfun.js").tfun; if (!tfun) { tfun = global.tfun; } export default class Sync { /** * * @param {Database} db */ constructor(db) { this.db = db; } async _fetch(lastSyncedTimestamp) { let token = await this.db.user.token(); if (!token) throw new Error("You are not logged in"); let response = await fetch(`${HOST}sync?lst=${lastSyncedTimestamp}`, { headers: { ...HEADERS, Authorization: `Bearer ${token}` }, }); //TODO decrypt the response. return await response.json(); } async throwOnConflicts() { let hasConflicts = await this.db.context.read("hasConflicts"); if (hasConflicts) { const mergeConflictError = new Error( "Merge conflicts detected. Please resolve all conflicts to continue syncing." ); mergeConflictError.code = "MERGE_CONFLICT"; throw mergeConflictError; } } async start() { let user = await this.db.user.get(); if (!user) throw new Error("You need to login to sync."); await this.db.conflicts.recalculate(); await this.throwOnConflicts(); let lastSyncedTimestamp = user.lastSynced || 0; let serverResponse = await this._fetch(lastSyncedTimestamp); // we prepare local data before merging so we always have correct data const prepare = new Prepare(this.db, user); const data = await prepare.get(lastSyncedTimestamp); // merge the server response const merger = new Merger(this.db, lastSyncedTimestamp); await merger.merge(serverResponse); await this.throwOnConflicts(); // send the data back to server const lastSynced = await this._send(data); // update our lastSynced time if (lastSynced) await this.db.user.set({ lastSynced }); } async _send(data) { //TODO encrypt the payload let token = await this.db.user.token(); if (!token) return; let response = await fetch(`${HOST}sync`, { method: "POST", headers: { ...HEADERS, Authorization: `Bearer ${token}` }, body: JSON.stringify(data), }); if (response.ok) { const json = await response.json(); return json.lastSynced; } throw new Error("Failed to sync with the server."); } }