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

149 lines
4.1 KiB
JavaScript

import { migrations } from "../../migrations";
import { areAllEmpty } from "./utils";
import SparkMD5 from "spark-md5";
class Merger {
/**
*
* @param {import("../index").default} db
*/
constructor(db) {
this._db = db;
}
_migrate(item, deserialized) {
// it is a locked note, bail out.
if (deserialized.alg && deserialized.cipher) return deserialized;
const version = item.v || 0;
let type = deserialized.type;
if (!type && deserialized.data) type = "tiny";
const migrate = migrations[version][type];
if (migrate) return migrate(deserialized);
return deserialized;
}
async _deserialize(item, migrate = true) {
const deserialized = JSON.parse(
await this._db.context.decrypt(this.key, item)
);
deserialized.remote = true;
if (!migrate) return deserialized;
return this._migrate(item, deserialized);
}
async _mergeItem(remoteItem, get, add) {
let localItem = await get(remoteItem.id);
remoteItem = await this._deserialize(remoteItem);
if (!localItem || remoteItem.dateEdited > localItem.dateEdited) {
await add(remoteItem);
}
}
async _mergeArray(array, get, set) {
if (!array) return;
for (let item of array) {
await this._mergeItem(item, get, set);
}
}
async _mergeItemWithConflicts(remoteItem, get, add, resolve) {
let localItem = await get(remoteItem.id);
remoteItem = await this._deserialize(remoteItem);
if (!localItem) {
await add(remoteItem);
} else if (!localItem.resolved && localItem.dateEdited > this._lastSynced) {
await resolve(localItem, remoteItem);
} else if (localItem.resolved) {
await add({ ...localItem, resolved: false });
} else {
await add(remoteItem);
}
}
async _mergeArrayWithConflicts(array, get, set, resolve) {
if (!array) return;
return Promise.all(
array.map(
async (item) =>
await this._mergeItemWithConflicts(item, get, set, resolve)
)
);
}
async merge(serverResponse, lastSynced) {
if (!serverResponse) return false;
this._lastSynced = lastSynced;
const { notes, synced, notebooks, content, trash, vaultKey, settings } =
serverResponse;
if (synced || areAllEmpty(serverResponse)) return false;
this.key = await this._db.user.getEncryptionKey();
if (vaultKey) {
await this._db.vault._setKey(await this._deserialize(vaultKey, false));
}
await this._mergeArray(
settings,
() => this._db.settings.raw,
(item) => this._db.settings.merge(item)
);
await this._mergeArray(
notes,
(id) => this._db.notes.note(id),
(item) => this._db.notes.add(item)
);
await this._mergeArray(
notebooks,
(id) => this._db.notebooks.notebook(id),
(item) => this._db.notebooks.merge(item)
);
await this._mergeArrayWithConflicts(
content,
(id) => this._db.content.raw(id),
(item) => this._db.content.add(item),
async (local, remote) => {
// if hashes are equal do nothing
if (
!remote ||
!local ||
!remote.data ||
remote.data === "undefined" || //TODO not sure about this
SparkMD5.hash(local.data) === SparkMD5.hash(remote.data)
)
return;
let note = this._db.notes.note(local.noteId);
if (!note || !note.data) return;
note = note.data;
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.context.write("hasConflicts", true);
}
}
);
await this._mergeArray(
trash,
() => undefined,
(item) => this._db.trash.add(item)
);
return true;
}
}
export default Merger;