2020-04-09 16:36:57 +05:00
|
|
|
/**
|
|
|
|
|
* GENERAL PROCESS:
|
2020-04-16 03:04:44 +05:00
|
|
|
* make a get request to server with current lastSynced
|
2020-04-09 16:36:57 +05:00
|
|
|
* 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:
|
2020-04-16 03:04:44 +05:00
|
|
|
* Locally, get everything that was editted/added after the lastSynced
|
2020-04-09 16:36:57 +05:00
|
|
|
* 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
|
2020-04-16 03:04:44 +05:00
|
|
|
* Update items that were editted before the lastSynced
|
|
|
|
|
* Try to merge items that were edited after the lastSynced
|
2020-04-09 16:36:57 +05:00
|
|
|
* 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.
|
|
|
|
|
*/
|
2021-10-26 23:06:52 +05:00
|
|
|
import {
|
|
|
|
|
checkIsUserPremium,
|
|
|
|
|
CHECK_IDS,
|
|
|
|
|
EV,
|
|
|
|
|
EVENTS,
|
|
|
|
|
sendAttachmentsProgressEvent,
|
|
|
|
|
} from "../../common";
|
2020-09-19 11:46:36 +05:00
|
|
|
import Constants from "../../utils/constants";
|
2020-12-16 12:06:25 +05:00
|
|
|
import http from "../../utils/http";
|
|
|
|
|
import TokenManager from "../token-manager";
|
2020-04-16 03:04:44 +05:00
|
|
|
import Collector from "./collector";
|
2020-04-09 16:36:57 +05:00
|
|
|
import Merger from "./merger";
|
2021-06-16 10:08:29 +05:00
|
|
|
import { areAllEmpty } from "./utils";
|
2021-10-26 23:06:52 +05:00
|
|
|
import { Mutex, withTimeout } from "async-mutex";
|
|
|
|
|
|
2020-04-09 16:36:57 +05:00
|
|
|
export default class Sync {
|
|
|
|
|
/**
|
|
|
|
|
*
|
2020-04-16 03:04:44 +05:00
|
|
|
* @param {import("../index").default} db
|
2020-04-09 16:36:57 +05:00
|
|
|
*/
|
|
|
|
|
constructor(db) {
|
2020-04-16 03:04:44 +05:00
|
|
|
this._db = db;
|
|
|
|
|
this._collector = new Collector(this._db);
|
|
|
|
|
this._merger = new Merger(this._db);
|
2021-09-26 11:47:13 +05:00
|
|
|
this._tokenManager = new TokenManager(this._db.storage);
|
2021-10-26 23:06:52 +05:00
|
|
|
this._autoSyncTimeout = 0;
|
2021-10-29 11:55:18 +05:00
|
|
|
this._autoSyncInterval = 5000;
|
2021-11-18 15:28:36 +05:00
|
|
|
this._locked = false;
|
2020-04-09 16:36:57 +05:00
|
|
|
|
2021-10-26 23:06:52 +05:00
|
|
|
this.syncMutex = withTimeout(
|
|
|
|
|
new Mutex(),
|
|
|
|
|
20 * 1000,
|
|
|
|
|
new Error("Sync timed out.")
|
2020-12-16 12:06:25 +05:00
|
|
|
);
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|
|
|
|
|
|
2020-12-11 20:19:28 +05:00
|
|
|
async start(full, force) {
|
2021-10-26 23:06:52 +05:00
|
|
|
if (this.syncMutex.isLocked()) return false;
|
|
|
|
|
|
|
|
|
|
return this.syncMutex
|
|
|
|
|
.runExclusive(() => {
|
|
|
|
|
this.stopAutoSync();
|
|
|
|
|
return this._sync(full, force);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => this._afterSync());
|
|
|
|
|
}
|
2021-09-13 09:37:52 +05:00
|
|
|
|
2021-11-18 15:28:36 +05:00
|
|
|
async remoteSync() {
|
|
|
|
|
if (this.syncMutex.isLocked()) {
|
|
|
|
|
this.hasNewChanges = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.syncMutex
|
|
|
|
|
.runExclusive(async () => {
|
|
|
|
|
this.stopAutoSync();
|
|
|
|
|
this.hasNewChanges = false;
|
|
|
|
|
if (await this._sync(true, false))
|
|
|
|
|
EV.publish(EVENTS.appRefreshRequested);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => this._afterSync());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async startAutoSync() {
|
|
|
|
|
if (!(await checkIsUserPremium(CHECK_IDS.databaseSync))) return;
|
|
|
|
|
this.databaseUpdatedEvent = EV.subscribe(
|
|
|
|
|
EVENTS.databaseUpdated,
|
|
|
|
|
this._scheduleSync.bind(this)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopAutoSync() {
|
|
|
|
|
clearTimeout(this._autoSyncTimeout);
|
|
|
|
|
if (this.databaseUpdatedEvent) this.databaseUpdatedEvent.unsubscribe();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquireLock() {
|
|
|
|
|
this.stopAutoSync();
|
|
|
|
|
this._locked = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async releaseLock() {
|
|
|
|
|
this._locked = false;
|
|
|
|
|
await this.startAutoSync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isLocked() {
|
|
|
|
|
return this._locked;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-26 23:06:52 +05:00
|
|
|
async _sync(full, force) {
|
2021-11-18 15:28:36 +05:00
|
|
|
if (this.isLocked()) return false;
|
|
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
let { lastSynced } = await this._performChecks();
|
2021-10-26 23:06:52 +05:00
|
|
|
if (force) lastSynced = 0;
|
|
|
|
|
|
|
|
|
|
// We request and merge remote attachments beforehand to handle
|
|
|
|
|
// all possible conflicts that will occur if a user attaches
|
|
|
|
|
// the same file/image on both his devices. Since both files
|
|
|
|
|
// will have the same hash but different encryption key, it
|
|
|
|
|
// will cause problems on the local device.
|
2021-10-27 11:18:30 +05:00
|
|
|
await this._mergeAttachments(lastSynced);
|
2021-10-26 23:06:52 +05:00
|
|
|
|
|
|
|
|
// All pending attachments are uploaded before anything else.
|
|
|
|
|
// This is done to ensure that when any note arrives on user's
|
|
|
|
|
// device, its attachments can be downloaded.
|
|
|
|
|
await this._uploadAttachments();
|
|
|
|
|
|
|
|
|
|
// We collect, encrypt, and ready local changes before asking
|
|
|
|
|
// the server for remote changes. This is done to ensure we
|
|
|
|
|
// don't accidentally send the remote changes back to the server.
|
|
|
|
|
const data = await this._collector.collect(lastSynced);
|
2021-11-02 12:43:56 +05:00
|
|
|
|
|
|
|
|
// We update the local last synced time before fetching data
|
|
|
|
|
// from the server. This is necessary to ensure that if the user
|
|
|
|
|
// makes any local changes, it is not ignored in the next sync.
|
|
|
|
|
// This is also the last synced time that is set for later sync cycles.
|
|
|
|
|
data.lastSynced = Date.now();
|
2021-10-26 23:06:52 +05:00
|
|
|
if (full) {
|
|
|
|
|
// We request remote changes and merge them. If any new changes
|
|
|
|
|
// come before or during this step (e.g. SSE), it can be safely
|
|
|
|
|
// ignored because the `lastSynced` time can never be newer
|
|
|
|
|
// than the change time.
|
2021-10-27 11:18:30 +05:00
|
|
|
var serverResponse = await this._fetch(lastSynced);
|
2021-10-26 23:06:52 +05:00
|
|
|
await this._merger.merge(serverResponse, lastSynced);
|
2021-08-10 11:59:26 +05:00
|
|
|
await this._db.conflicts.check();
|
2021-10-26 23:06:52 +05:00
|
|
|
// ignore the changes that have arrived uptil this point.
|
2021-10-27 10:53:36 +05:00
|
|
|
// this.hasNewChanges = false;
|
2021-10-26 23:06:52 +05:00
|
|
|
}
|
2020-04-09 16:36:57 +05:00
|
|
|
|
2021-10-27 10:53:36 +05:00
|
|
|
if (!areAllEmpty(data)) {
|
2021-10-27 11:18:30 +05:00
|
|
|
lastSynced = await this._send(data);
|
2021-10-27 10:53:36 +05:00
|
|
|
} else if (serverResponse) lastSynced = serverResponse.lastSynced;
|
|
|
|
|
|
|
|
|
|
await this._db.storage.write("lastSynced", lastSynced);
|
2021-10-26 23:06:52 +05:00
|
|
|
return true;
|
|
|
|
|
}
|
2021-09-13 09:37:52 +05:00
|
|
|
|
2021-10-26 23:06:52 +05:00
|
|
|
async _afterSync() {
|
2021-10-27 10:53:36 +05:00
|
|
|
if (!this.hasNewChanges) {
|
|
|
|
|
this.startAutoSync();
|
|
|
|
|
} else {
|
|
|
|
|
return this.remoteSync();
|
|
|
|
|
}
|
2021-10-26 23:06:52 +05:00
|
|
|
}
|
2020-08-24 13:07:16 +05:00
|
|
|
|
2021-10-26 23:06:52 +05:00
|
|
|
_scheduleSync() {
|
|
|
|
|
this.stopAutoSync();
|
|
|
|
|
this._autoSyncTimeout = setTimeout(() => {
|
|
|
|
|
EV.publish(EVENTS.databaseSyncRequested);
|
2021-10-29 13:02:33 +05:00
|
|
|
}, this._autoSyncInterval);
|
2020-08-24 13:07:16 +05:00
|
|
|
}
|
|
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
async _send(data) {
|
|
|
|
|
let token = await this._tokenManager.getAccessToken();
|
2020-12-16 12:06:25 +05:00
|
|
|
let response = await http.post.json(
|
|
|
|
|
`${Constants.API_HOST}/sync`,
|
|
|
|
|
data,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
return response.lastSynced;
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|
2021-09-20 12:10:36 +05:00
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
async _mergeAttachments(lastSynced) {
|
|
|
|
|
let token = await this._tokenManager.getAccessToken();
|
2021-10-23 11:41:17 +05:00
|
|
|
var serverResponse = await this._fetchAttachments(lastSynced, token);
|
|
|
|
|
await this._merger.merge(serverResponse, lastSynced);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-29 09:53:50 +05:00
|
|
|
async _uploadAttachments() {
|
2021-09-20 12:10:36 +05:00
|
|
|
const attachments = this._db.attachments.pending;
|
2021-10-01 11:40:18 +05:00
|
|
|
try {
|
|
|
|
|
for (var i = 0; i < attachments.length; ++i) {
|
|
|
|
|
const attachment = attachments[i];
|
|
|
|
|
const { hash } = attachment.metadata;
|
2021-11-02 14:25:48 +05:00
|
|
|
sendAttachmentsProgressEvent("upload", hash, attachments.length, i);
|
2021-09-20 12:10:36 +05:00
|
|
|
|
2021-10-01 11:40:18 +05:00
|
|
|
const isUploaded = await this._db.fs.uploadFile(hash, hash);
|
|
|
|
|
if (!isUploaded) throw new Error("Failed to upload file.");
|
2021-09-20 12:10:36 +05:00
|
|
|
|
2021-10-01 11:40:18 +05:00
|
|
|
await this._db.attachments.markAsUploaded(attachment.id);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error("Failed to upload attachments. Error: " + e.message);
|
|
|
|
|
} finally {
|
2021-10-21 10:16:24 +05:00
|
|
|
sendAttachmentsProgressEvent("upload", null, attachments.length);
|
2021-09-26 11:47:13 +05:00
|
|
|
}
|
2021-09-20 12:10:36 +05:00
|
|
|
}
|
2021-10-26 23:06:52 +05:00
|
|
|
|
|
|
|
|
async _performChecks() {
|
|
|
|
|
let lastSynced = (await this._db.lastSynced()) || 0;
|
|
|
|
|
|
|
|
|
|
// update the conflicts status and if find any, throw
|
|
|
|
|
await this._db.conflicts.recalculate();
|
|
|
|
|
await this._db.conflicts.check();
|
|
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
return { lastSynced };
|
2021-10-26 23:06:52 +05:00
|
|
|
}
|
|
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
async _fetch(lastSynced) {
|
|
|
|
|
let token = await this._tokenManager.getAccessToken();
|
2021-10-26 23:06:52 +05:00
|
|
|
return await http.get(
|
|
|
|
|
`${Constants.API_HOST}/sync?lst=${lastSynced}`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-27 11:18:30 +05:00
|
|
|
async _fetchAttachments(lastSynced) {
|
|
|
|
|
let token = await this._tokenManager.getAccessToken();
|
2021-10-26 23:06:52 +05:00
|
|
|
return await http.get(
|
|
|
|
|
`${Constants.API_HOST}/sync/attachments?lst=${lastSynced}`,
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|