Files
notesnook/packages/core/api/sync/merger.js

189 lines
5.5 KiB
JavaScript
Raw Normal View History

2020-12-06 11:13:17 +05:00
import { migrations } from "../../migrations";
2020-04-09 16:36:57 +05:00
import { areAllEmpty } from "./utils";
import SparkMD5 from "spark-md5";
import setManipulator from "../../utils/set";
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
*/
constructor(db) {
2020-04-09 16:36:57 +05:00
this._db = db;
}
2021-07-06 20:56:54 +05:00
_migrate(deserialized, version) {
// 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
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) {
const deserialized = JSON.parse(
await this._db.storage.decrypt(this.key, item)
);
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-09 16:36:57 +05:00
async _mergeItem(remoteItem, get, add) {
let localItem = await get(remoteItem.id);
remoteItem = await this._deserialize(remoteItem);
2020-04-09 16:36:57 +05:00
if (!localItem || remoteItem.dateEdited > localItem.dateEdited) {
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
}
async _mergeItemWithConflicts(remoteItem, get, add, markAsConflicted) {
2020-04-09 16:36:57 +05:00
let localItem = await get(remoteItem.id);
remoteItem = await this._deserialize(remoteItem);
if (!localItem) {
await add(remoteItem);
} else if (
localItem.dateResolved !== remoteItem.dateEdited &&
localItem.dateEdited > this._lastSynced
) {
// 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.dateEdited, localItem.dateEdited) -
Math.min(remoteItem.dateEdited, localItem.dateEdited);
const ONE_MINUTE = 60 * 1000;
if (timeDiff < ONE_MINUTE) {
if (remoteItem.dateEdited > localItem.dateEdited) await add(remoteItem);
return;
}
await markAsConflicted(localItem, remoteItem);
} else {
await add(remoteItem);
}
2020-04-09 16:36:57 +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
}
async merge(serverResponse, lastSynced) {
2020-04-09 16:36:57 +05:00
if (!serverResponse) return false;
this._lastSynced = lastSynced;
2021-09-20 12:10:36 +05:00
const {
notes = [],
2021-09-20 12:10:36 +05:00
synced,
notebooks = [],
content = [],
vaultKey,
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
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
}
await this._mergeArrayWithConflicts(attachments, async (item) => {
const remoteAttachment = await this._deserialize(item);
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
);
}
await this._db.attachments.add(remoteAttachment);
});
2021-09-20 12:10:36 +05:00
2020-12-05 11:26:02 +05:00
await this._mergeArray(
settings,
() => this._db.settings.raw,
(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,
(id) => this._db.notes.note(id),
(item) => this._db.notes.add(item)
);
2020-12-05 11:26:02 +05:00
2020-04-09 16:36:57 +05:00
await this._mergeArray(
notebooks,
(id) => this._db.notebooks.notebook(id),
2021-02-25 19:41:17 +05:00
(item) => this._db.notebooks.merge(item)
2020-04-09 16:36:57 +05:00
);
await this._mergeArrayWithConflicts(content, (item) =>
this._mergeItemWithConflicts(
item,
(id) => this._db.content.raw(id, false),
(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;
// if hashes are equal do nothing
if (
!note.locked &&
(!remote ||
!local ||
!local.data ||
!remote.data ||
remote.data === "undefined" || //TODO not sure about this
SparkMD5.hash(local.data) === SparkMD5.hash(remote.data))
)
return;
if (remote.deleted || local.deleted || note.locked) {
// if note is locked or content is deleted we keep the most recent version.
if (remote.dateEdited > local.dateEdited)
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-09 16:36:57 +05:00
);
return true;
}
}
export default Merger;