trilium/src/services/sync_update.js
2023-12-30 00:34:46 +01:00

176 lines
5.9 KiB
JavaScript

const sql = require('./sql.js');
const log = require('./log.js');
const entityChangesService = require('./entity_changes.js');
const eventService = require('./events.js');
const entityConstructor = require('../becca/entity_constructor.js');
const ws = require('./ws.js');
function updateEntities(entityChanges, instanceId) {
if (entityChanges.length === 0) {
return;
}
let atLeastOnePullApplied = false;
const updateContext = {
updated: {},
alreadyUpdated: 0,
erased: 0,
alreadyErased: 0
};
for (const {entityChange, entity} of entityChanges) {
const changeAppliedAlready = entityChange.changeId
&& !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
if (changeAppliedAlready) {
updateContext.alreadyUpdated++;
continue;
}
if (!atLeastOnePullApplied) { // avoid spamming and send only for first
ws.syncPullInProgress();
atLeastOnePullApplied = true;
}
updateEntity(entityChange, entity, instanceId, updateContext);
}
logUpdateContext(updateContext);
}
function updateEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
if (!remoteEntityRow && remoteEC.entityName === 'options') {
return; // can be undefined for options with isSynced=false
}
const updated = remoteEC.entityName === 'note_reordering'
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
if (updated) {
if (remoteEntityRow?.isDeleted) {
eventService.emit(eventService.ENTITY_DELETE_SYNCED, {
entityName: remoteEC.entityName,
entityId: remoteEC.entityId
});
}
else if (!remoteEC.isErased) {
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
entityName: remoteEC.entityName,
entityRow: remoteEntityRow
});
}
}
}
function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext) {
const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) {
if (remoteEC.isErased) {
if (localEC?.isErased) {
eraseEntity(remoteEC); // make sure it's erased anyway
updateContext.alreadyErased++;
return false; // we won't save entitychange in this case
} else {
eraseEntity(remoteEC);
updateContext.erased++;
}
} else {
if (!remoteEntityRow) {
throw new Error(`Empty entity row for: ${JSON.stringify(remoteEC)}`);
}
preProcessContent(remoteEC, remoteEntityRow);
sql.replace(remoteEC.entityName, remoteEntityRow);
updateContext.updated[remoteEC.entityName] = updateContext.updated[remoteEC.entityName] || [];
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
}
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged
|| localEC.hash !== remoteEC.hash
|| localEC.isErased !== remoteEC.isErased
) {
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
}
return true;
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
&& localEC.utcDateChanged > remoteEC.utcDateChanged) {
// the change on our side is newer than on the other side, so the other side should update
entityChangesService.putEntityChangeForOtherInstances(localEC);
return false;
}
return false;
}
function preProcessContent(remoteEC, remoteEntityRow) {
if (remoteEC.entityName === 'blobs' && remoteEntityRow.content !== null) {
// we always use a Buffer object which is different from normal saving - there we use a simple string type for
// "string notes". The problem is that in general, it's not possible to detect whether a blob content
// is string note or note (syncs can arrive out of order)
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (remoteEntityRow.content.byteLength === 0) {
// there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
// (possibly not a problem anymore with the newer better-sqlite3)
remoteEntityRow.content = "";
}
}
}
function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
if (!remoteEntityRow) {
throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`);
}
for (const key in remoteEntityRow) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key], key]);
}
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
return true;
}
function eraseEntity(entityChange) {
const {entityName, entityId} = entityChange;
const entityNames = [
"notes",
"branches",
"attributes",
"revisions",
"attachments",
"blobs"
];
if (!entityNames.includes(entityName)) {
log.error(`Cannot erase ${entityName} '${entityId}'.`);
return;
}
const primaryKeyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
}
function logUpdateContext(updateContext) {
const message = JSON.stringify(updateContext)
.replaceAll('"', '')
.replaceAll(":", ": ")
.replaceAll(",", ", ");
log.info(message.substr(1, message.length - 2));
}
module.exports = {
updateEntities
};