2020-12-06 11:13:17 +05:00
|
|
|
import { migrations } from "../../migrations";
|
2020-04-09 16:36:57 +05:00
|
|
|
import { areAllEmpty } from "./utils";
|
2021-02-12 10:01:06 +05:00
|
|
|
import SparkMD5 from "spark-md5";
|
2021-10-23 11:41:17 +05:00
|
|
|
import setManipulator from "../../utils/set";
|
2021-12-31 11:50:21 +05:00
|
|
|
import { CURRENT_DATABASE_VERSION } from "../../common";
|
2020-04-09 16:36:57 +05:00
|
|
|
|
|
|
|
|
class Merger {
|
|
|
|
|
/**
|
|
|
|
|
*
|
2020-11-25 00:48:02 +05:00
|
|
|
* @param {import("../index").default} db
|
2020-04-09 16:36:57 +05:00
|
|
|
*/
|
2020-04-16 03:04:44 +05:00
|
|
|
constructor(db) {
|
2020-04-09 16:36:57 +05:00
|
|
|
this._db = db;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-06 20:56:54 +05:00
|
|
|
_migrate(deserialized, version) {
|
2021-02-18 09:30:48 +05:00
|
|
|
// it is a locked note, bail out.
|
|
|
|
|
if (deserialized.alg && deserialized.cipher) return deserialized;
|
|
|
|
|
|
2020-12-06 14:50:01 +05:00
|
|
|
let type = deserialized.type;
|
2021-02-12 10:15:37 +05:00
|
|
|
if (!type && deserialized.data) type = "tiny";
|
2021-03-06 09:36:50 +05:00
|
|
|
|
2021-12-31 11:50:21 +05:00
|
|
|
if (!migrations[version]) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
version > CURRENT_DATABASE_VERSION
|
|
|
|
|
? `Cannot migrate item to v${version}. Please update your client to the latest version.`
|
|
|
|
|
: `Could not migrate item to v${version}. Please report this bug to the developers.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-06 14:50:01 +05:00
|
|
|
const migrate = migrations[version][type];
|
2020-12-06 11:13:17 +05:00
|
|
|
if (migrate) return migrate(deserialized);
|
|
|
|
|
return deserialized;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-06 09:36:50 +05:00
|
|
|
async _deserialize(item, migrate = true) {
|
2022-01-03 14:54:23 +05:00
|
|
|
const decrypted = await this._db.storage.decrypt(this.key, item);
|
|
|
|
|
if (!decrypted) {
|
|
|
|
|
throw new Error("Decrypted item cannot be undefined.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deserialized = JSON.parse(decrypted);
|
2020-04-13 12:04:49 +05:00
|
|
|
deserialized.remote = true;
|
2021-03-06 09:36:50 +05:00
|
|
|
if (!migrate) return deserialized;
|
2021-07-06 20:56:54 +05:00
|
|
|
return this._migrate(deserialized, item.v);
|
2020-04-13 12:04:49 +05:00
|
|
|
}
|
|
|
|
|
|
2020-04-09 16:36:57 +05:00
|
|
|
async _mergeItem(remoteItem, get, add) {
|
2020-04-13 12:04:49 +05:00
|
|
|
remoteItem = await this._deserialize(remoteItem);
|
2022-01-12 23:51:21 +05:00
|
|
|
let localItem = await get(remoteItem.id);
|
2022-03-11 15:06:21 +05:00
|
|
|
if (localItem && localItem.localOnly) return;
|
2022-01-12 23:51:21 +05:00
|
|
|
|
2021-12-20 14:37:06 +05:00
|
|
|
if (!localItem || remoteItem.dateModified > localItem.dateModified) {
|
2020-04-09 16:36:57 +05:00
|
|
|
await add(remoteItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _mergeArray(array, get, set) {
|
|
|
|
|
if (!array) return;
|
2021-02-20 09:50:22 +05:00
|
|
|
for (let item of array) {
|
|
|
|
|
await this._mergeItem(item, get, set);
|
|
|
|
|
}
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|
|
|
|
|
|
2021-08-10 11:59:56 +05:00
|
|
|
async _mergeItemWithConflicts(remoteItem, get, add, markAsConflicted) {
|
2020-04-13 12:04:49 +05:00
|
|
|
remoteItem = await this._deserialize(remoteItem);
|
2022-01-12 23:51:21 +05:00
|
|
|
let localItem = await get(remoteItem.id);
|
2022-03-11 15:06:21 +05:00
|
|
|
if (localItem && localItem.localOnly) return;
|
2021-12-31 10:45:10 +05:00
|
|
|
|
2020-04-11 11:42:17 +05:00
|
|
|
if (!localItem) {
|
|
|
|
|
await add(remoteItem);
|
2022-01-01 20:58:56 +05:00
|
|
|
} else {
|
|
|
|
|
const isResolved = localItem.dateResolved === remoteItem.dateModified;
|
|
|
|
|
const isModified = localItem.dateModified > this._lastSynced;
|
|
|
|
|
|
|
|
|
|
if (isModified && !isResolved) {
|
|
|
|
|
// If time difference between local item's edits & remote item's edits
|
|
|
|
|
// is less than 1 minute, we shouldn't trigger a merge conflict; instead
|
|
|
|
|
// we will keep the most recently changed item.
|
|
|
|
|
const timeDiff =
|
|
|
|
|
Math.max(remoteItem.dateModified, localItem.dateModified) -
|
|
|
|
|
Math.min(remoteItem.dateModified, localItem.dateModified);
|
|
|
|
|
const ONE_MINUTE = 60 * 1000;
|
|
|
|
|
if (timeDiff < ONE_MINUTE) {
|
|
|
|
|
if (remoteItem.dateModified > localItem.dateModified) {
|
|
|
|
|
await add(remoteItem);
|
|
|
|
|
}
|
|
|
|
|
return;
|
2021-12-31 10:45:10 +05:00
|
|
|
}
|
2021-10-27 10:53:36 +05:00
|
|
|
|
2022-01-01 20:58:56 +05:00
|
|
|
await markAsConflicted(localItem, remoteItem);
|
|
|
|
|
} else if (!isResolved) {
|
|
|
|
|
await add(remoteItem);
|
|
|
|
|
}
|
2020-04-11 12:07:40 +05:00
|
|
|
}
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|
|
|
|
|
|
2021-10-23 11:41:17 +05:00
|
|
|
async _mergeArrayWithConflicts(array, mergeItem) {
|
2020-04-09 16:36:57 +05:00
|
|
|
if (!array) return;
|
2021-10-26 12:14:37 +05:00
|
|
|
for (let item of array) {
|
|
|
|
|
await mergeItem(item);
|
|
|
|
|
}
|
2020-04-09 16:36:57 +05:00
|
|
|
}
|
|
|
|
|
|
2020-04-16 03:04:44 +05:00
|
|
|
async merge(serverResponse, lastSynced) {
|
2020-04-09 16:36:57 +05:00
|
|
|
if (!serverResponse) return false;
|
2020-04-16 03:04:44 +05:00
|
|
|
this._lastSynced = lastSynced;
|
2021-09-20 12:10:36 +05:00
|
|
|
const {
|
2021-10-23 11:41:17 +05:00
|
|
|
notes = [],
|
2021-09-20 12:10:36 +05:00
|
|
|
synced,
|
2021-10-23 11:41:17 +05:00
|
|
|
notebooks = [],
|
|
|
|
|
content = [],
|
2021-10-25 11:35:00 +05:00
|
|
|
vaultKey,
|
2021-10-23 11:41:17 +05:00
|
|
|
settings = [],
|
|
|
|
|
attachments = [],
|
2021-09-20 12:10:36 +05:00
|
|
|
} = serverResponse;
|
2020-04-09 16:36:57 +05:00
|
|
|
|
|
|
|
|
if (synced || areAllEmpty(serverResponse)) return false;
|
2020-12-16 12:06:25 +05:00
|
|
|
this.key = await this._db.user.getEncryptionKey();
|
2020-04-09 16:36:57 +05:00
|
|
|
|
2022-01-03 14:54:23 +05:00
|
|
|
if (!this.key.key || !this.key.salt) {
|
|
|
|
|
await this._db.user.logout(true, "User encryption key not generated.");
|
|
|
|
|
throw new Error("User encryption key not generated. Please relogin.");
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-15 14:00:05 +05:00
|
|
|
if (vaultKey) {
|
2021-03-06 09:36:50 +05:00
|
|
|
await this._db.vault._setKey(await this._deserialize(vaultKey, false));
|
2020-04-15 14:00:05 +05:00
|
|
|
}
|
|
|
|
|
|
2021-10-23 11:41:17 +05:00
|
|
|
await this._mergeArrayWithConflicts(attachments, async (item) => {
|
|
|
|
|
const remoteAttachment = await this._deserialize(item);
|
2022-03-16 22:12:41 +05:00
|
|
|
if (remoteAttachment.deleted) {
|
|
|
|
|
await this._db.attachments.merge(remoteAttachment);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-23 11:41:17 +05:00
|
|
|
const localAttachment = this._db.attachments.attachment(
|
|
|
|
|
remoteAttachment.metadata.hash
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
localAttachment &&
|
|
|
|
|
localAttachment.dateUploaded !== remoteAttachment.dateUploaded
|
|
|
|
|
) {
|
|
|
|
|
const noteIds = localAttachment.noteIds.slice();
|
|
|
|
|
const isRemoved = await this._db.attachments.remove(
|
|
|
|
|
localAttachment.metadata.hash,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
if (!isRemoved)
|
|
|
|
|
throw new Error(
|
|
|
|
|
"Conflict could not be resolved in one of the attachments."
|
|
|
|
|
);
|
|
|
|
|
remoteAttachment.noteIds = setManipulator.union(
|
|
|
|
|
remoteAttachment.noteIds,
|
|
|
|
|
noteIds
|
|
|
|
|
);
|
|
|
|
|
}
|
2021-12-01 20:18:14 +05:00
|
|
|
await this._db.attachments.merge(remoteAttachment);
|
2021-10-23 11:41:17 +05:00
|
|
|
});
|
2021-09-20 12:10:36 +05:00
|
|
|
|
2020-12-05 11:26:02 +05:00
|
|
|
await this._mergeArray(
|
|
|
|
|
settings,
|
|
|
|
|
() => this._db.settings.raw,
|
2020-12-10 12:58:04 +05:00
|
|
|
(item) => this._db.settings.merge(item)
|
2020-12-05 11:26:02 +05:00
|
|
|
);
|
|
|
|
|
|
2020-04-09 16:36:57 +05:00
|
|
|
await this._mergeArray(
|
|
|
|
|
notes,
|
2022-03-11 15:06:21 +05:00
|
|
|
(id) => {
|
|
|
|
|
const note = this._db.notes.note(id);
|
|
|
|
|
if (note) return note.data;
|
|
|
|
|
return;
|
|
|
|
|
},
|
2021-12-20 09:28:17 +05:00
|
|
|
(item) => this._db.notes.merge(item)
|
2020-04-09 16:36:57 +05:00
|
|
|
);
|
2020-12-05 11:26:02 +05:00
|
|
|
|
2020-04-09 16:36:57 +05:00
|
|
|
await this._mergeArray(
|
|
|
|
|
notebooks,
|
2022-03-11 15:06:21 +05:00
|
|
|
(id) => {
|
|
|
|
|
const notebook = this._db.notebooks.notebook(id);
|
|
|
|
|
if (notebook) return notebook.data;
|
|
|
|
|
return;
|
|
|
|
|
},
|
2021-02-25 19:41:17 +05:00
|
|
|
(item) => this._db.notebooks.merge(item)
|
2020-04-09 16:36:57 +05:00
|
|
|
);
|
|
|
|
|
|
2021-10-23 11:41:17 +05:00
|
|
|
await this._mergeArrayWithConflicts(content, (item) =>
|
|
|
|
|
this._mergeItemWithConflicts(
|
|
|
|
|
item,
|
2021-12-29 09:34:20 +05:00
|
|
|
(id) => this._db.content.raw(id),
|
2021-10-23 11:41:17 +05:00
|
|
|
(item) => this._db.content.add(item),
|
|
|
|
|
async (local, remote) => {
|
|
|
|
|
let note = this._db.notes.note(local.noteId);
|
|
|
|
|
if (!note || !note.data) return;
|
|
|
|
|
note = note.data;
|
|
|
|
|
|
2022-02-25 15:27:00 +05:00
|
|
|
const isRemoteObject = typeof remote.data === "object";
|
|
|
|
|
const isLocalObject = typeof local.data === "object";
|
|
|
|
|
const localHash = isLocalObject ? null : SparkMD5.hash(local.data);
|
|
|
|
|
const remoteHash = isRemoteObject ? null : SparkMD5.hash(remote.data);
|
|
|
|
|
if (!note.locked) {
|
|
|
|
|
// reject all invalid states
|
|
|
|
|
if (
|
|
|
|
|
isRemoteObject || // no point in accepting invalid remote object
|
2021-10-23 11:41:17 +05:00
|
|
|
!local.data ||
|
|
|
|
|
!remote.data ||
|
2022-02-25 15:27:00 +05:00
|
|
|
localHash === remoteHash
|
|
|
|
|
)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// if we have an invalid content locally but the remote one is valid,
|
|
|
|
|
// let's accept the remote content.
|
|
|
|
|
if (isLocalObject && !isRemoteObject) {
|
|
|
|
|
await this._db.content.add({ id: local.id, ...remote });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-23 11:41:17 +05:00
|
|
|
|
|
|
|
|
if (remote.deleted || local.deleted || note.locked) {
|
|
|
|
|
// if note is locked or content is deleted we keep the most recent version.
|
2021-12-20 14:37:06 +05:00
|
|
|
if (remote.dateModified > local.dateModified)
|
2021-10-23 11:41:17 +05:00
|
|
|
await this._db.content.add({ id: local.id, ...remote });
|
|
|
|
|
} else {
|
|
|
|
|
// otherwise we trigger the conflicts
|
|
|
|
|
await this._db.content.add({ ...local, conflicted: remote });
|
|
|
|
|
await this._db.notes.add({ id: local.noteId, conflicted: true });
|
|
|
|
|
await this._db.storage.write("hasConflicts", true);
|
|
|
|
|
}
|
2020-04-15 17:50:26 +05:00
|
|
|
}
|
2021-10-23 11:41:17 +05:00
|
|
|
)
|
2020-04-09 16:36:57 +05:00
|
|
|
);
|
|
|
|
|
|
2021-11-12 10:32:25 +05:00
|
|
|
await this._db.notes.repairReferences();
|
2021-11-12 11:47:03 +05:00
|
|
|
await this._db.notebooks.repairReferences();
|
2020-04-09 16:36:57 +05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export default Merger;
|