2024-02-18 12:40:30 +02:00
|
|
|
import sql = require('./sql');
|
|
|
|
import log = require('./log');
|
|
|
|
import entityChangesService = require('./entity_changes');
|
|
|
|
import eventService = require('./events');
|
|
|
|
import entityConstructor = require('../becca/entity_constructor');
|
|
|
|
import ws = require('./ws');
|
|
|
|
import { EntityChange } from './entity_changes_interface';
|
|
|
|
|
|
|
|
interface EntityRow {
|
|
|
|
isDeleted?: boolean;
|
|
|
|
content: Buffer | string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface EntityChangeInput {
|
|
|
|
entityChange: EntityChange;
|
|
|
|
entity: EntityRow;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface UpdateContext {
|
|
|
|
alreadyErased: number;
|
|
|
|
erased: number;
|
|
|
|
updated: Record<string, string[]>
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateEntities(entityChanges: EntityChangeInput[], instanceId: string) {
|
2023-09-21 18:13:14 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
|
2023-07-29 21:59:20 +02:00
|
|
|
if (!remoteEntityRow && remoteEC.entityName === 'options') {
|
|
|
|
return; // can be undefined for options with isSynced=false
|
2020-03-08 21:59:19 +01:00
|
|
|
}
|
|
|
|
|
2023-07-29 21:59:20 +02:00
|
|
|
const updated = remoteEC.entityName === 'note_reordering'
|
|
|
|
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
|
2023-09-21 18:13:14 +02:00
|
|
|
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext);
|
2019-01-03 23:27:10 +01:00
|
|
|
|
2021-07-22 20:19:44 +02:00
|
|
|
if (updated) {
|
2023-07-29 21:59:20 +02:00
|
|
|
if (remoteEntityRow?.isDeleted) {
|
2021-05-01 11:38:20 +02:00
|
|
|
eventService.emit(eventService.ENTITY_DELETE_SYNCED, {
|
2023-07-29 21:59:20 +02:00
|
|
|
entityName: remoteEC.entityName,
|
|
|
|
entityId: remoteEC.entityId
|
2021-05-01 11:38:20 +02:00
|
|
|
});
|
|
|
|
}
|
2023-07-29 21:59:20 +02:00
|
|
|
else if (!remoteEC.isErased) {
|
2021-05-01 11:38:20 +02:00
|
|
|
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
|
2023-07-29 21:59:20 +02:00
|
|
|
entityName: remoteEC.entityName,
|
|
|
|
entityRow: remoteEntityRow
|
2021-05-01 11:38:20 +02:00
|
|
|
});
|
|
|
|
}
|
2019-01-03 23:27:10 +01:00
|
|
|
}
|
2018-04-07 22:25:28 -04:00
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string, updateContext: UpdateContext) {
|
|
|
|
const localEC = sql.getRow<EntityChange>(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
|
|
|
|
|
|
|
|
if (!localEC.utcDateChanged || !remoteEC.utcDateChanged) {
|
|
|
|
throw new Error("Missing date changed.");
|
|
|
|
}
|
2020-12-14 13:58:02 +01:00
|
|
|
|
2023-09-05 00:30:09 +02:00
|
|
|
if (!localEC || localEC.utcDateChanged <= remoteEC.utcDateChanged) {
|
2023-10-18 09:37:36 +02:00
|
|
|
if (remoteEC.isErased) {
|
|
|
|
if (localEC?.isErased) {
|
|
|
|
eraseEntity(remoteEC); // make sure it's erased anyway
|
|
|
|
updateContext.alreadyErased++;
|
|
|
|
} else {
|
|
|
|
eraseEntity(remoteEC);
|
|
|
|
updateContext.erased++;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!remoteEntityRow) {
|
|
|
|
throw new Error(`Empty entity row for: ${JSON.stringify(remoteEC)}`);
|
2023-07-27 23:22:08 +02:00
|
|
|
}
|
2019-11-01 20:00:56 +01:00
|
|
|
|
2023-10-18 09:37:36 +02:00
|
|
|
preProcessContent(remoteEC, remoteEntityRow);
|
2019-11-01 20:00:56 +01:00
|
|
|
|
2023-10-18 09:37:36 +02:00
|
|
|
sql.replace(remoteEC.entityName, remoteEntityRow);
|
|
|
|
|
|
|
|
updateContext.updated[remoteEC.entityName] = updateContext.updated[remoteEC.entityName] || [];
|
|
|
|
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
|
|
|
|
}
|
2023-09-21 18:13:14 +02:00
|
|
|
|
2024-01-10 23:51:53 +01:00
|
|
|
if (!localEC
|
|
|
|
|| localEC.utcDateChanged < remoteEC.utcDateChanged
|
2023-12-30 00:34:46 +01:00
|
|
|
|| localEC.hash !== remoteEC.hash
|
|
|
|
|| localEC.isErased !== remoteEC.isErased
|
|
|
|
) {
|
2023-09-05 00:30:09 +02:00
|
|
|
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
|
|
|
|
}
|
2019-11-01 20:00:56 +01:00
|
|
|
|
2020-04-04 14:57:19 +02:00
|
|
|
return true;
|
2023-12-30 00:34:46 +01:00
|
|
|
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
|
|
|
|
&& localEC.utcDateChanged > remoteEC.utcDateChanged) {
|
2023-07-29 21:59:20 +02:00
|
|
|
// the change on our side is newer than on the other side, so the other side should update
|
2023-07-29 23:25:02 +02:00
|
|
|
entityChangesService.putEntityChangeForOtherInstances(localEC);
|
2023-07-29 21:59:20 +02:00
|
|
|
|
|
|
|
return false;
|
2020-04-04 14:57:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2019-11-01 20:00:56 +01:00
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
|
2023-10-18 09:37:36 +02:00
|
|
|
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)
|
2024-02-18 12:40:30 +02:00
|
|
|
if (typeof remoteEntityRow.content === "string") {
|
|
|
|
remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
|
2023-10-18 09:37:36 +02:00
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
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 = "";
|
|
|
|
}
|
2023-10-18 09:37:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow, instanceId: string) {
|
2023-09-21 11:16:03 +02:00
|
|
|
if (!remoteEntityRow) {
|
|
|
|
throw new Error(`Empty note_reordering body for: ${JSON.stringify(remoteEC)}`);
|
|
|
|
}
|
|
|
|
|
2023-07-29 21:59:20 +02:00
|
|
|
for (const key in remoteEntityRow) {
|
2024-02-18 12:40:30 +02:00
|
|
|
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key as keyof EntityRow], key]);
|
2023-07-29 21:59:20 +02:00
|
|
|
}
|
2017-11-09 20:52:47 -05:00
|
|
|
|
2023-07-29 23:25:02 +02:00
|
|
|
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
|
2020-04-04 14:57:19 +02:00
|
|
|
|
|
|
|
return true;
|
2017-11-09 20:52:47 -05:00
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function eraseEntity(entityChange: EntityChange) {
|
2021-11-12 21:19:23 +01:00
|
|
|
const {entityName, entityId} = entityChange;
|
|
|
|
|
2023-01-22 23:36:05 +01:00
|
|
|
const entityNames = [
|
|
|
|
"notes",
|
|
|
|
"branches",
|
|
|
|
"attributes",
|
2023-06-04 23:01:40 +02:00
|
|
|
"revisions",
|
2023-03-16 12:17:55 +01:00
|
|
|
"attachments",
|
2023-07-29 21:59:20 +02:00
|
|
|
"blobs"
|
2023-01-22 23:36:05 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
if (!entityNames.includes(entityName)) {
|
2023-12-30 00:34:46 +01:00
|
|
|
log.error(`Cannot erase ${entityName} '${entityId}'.`);
|
2021-11-12 21:19:23 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-29 21:59:20 +02:00
|
|
|
const primaryKeyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
|
2021-11-12 21:19:23 +01:00
|
|
|
|
2023-07-29 21:59:20 +02:00
|
|
|
sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
|
2021-11-12 21:19:23 +01:00
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
function logUpdateContext(updateContext: UpdateContext) {
|
2023-09-21 18:13:14 +02:00
|
|
|
const message = JSON.stringify(updateContext)
|
|
|
|
.replaceAll('"', '')
|
|
|
|
.replaceAll(":", ": ")
|
|
|
|
.replaceAll(",", ", ");
|
|
|
|
|
|
|
|
log.info(message.substr(1, message.length - 2));
|
|
|
|
}
|
|
|
|
|
2024-02-18 12:40:30 +02:00
|
|
|
export = {
|
2023-09-21 18:13:14 +02:00
|
|
|
updateEntities
|
2020-06-20 12:31:38 +02:00
|
|
|
};
|