sync fixes and refactorings

This commit is contained in:
zadam 2023-07-29 21:59:20 +02:00
parent 2a7fe85020
commit 04b125afc0
14 changed files with 109 additions and 190 deletions

View File

@ -67,7 +67,6 @@ export default class TreeContextMenu {
{ title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [ { title: "Advanced", uiIcon: "bx bx-empty", enabled: true, items: [
{ title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes }, { title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes }, { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: "Force note sync", command: "forceNoteSync", uiIcon: "bx bx-refresh", enabled: noSelectedNotes },
{ title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch }, { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes }, { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes },
{ title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted } { title: 'Convert to attachment', command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted }

View File

@ -18,13 +18,6 @@ async function syncNow(ignoreNotConfigured = false) {
} }
} }
async function forceNoteSync(noteId) {
await server.post(`sync/force-note-sync/${noteId}`);
toastService.showMessage("Note added to sync queue.");
}
export default { export default {
syncNow, syncNow
forceNoteSync
}; };

View File

@ -1564,10 +1564,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.triggerCommand("showImportDialog", {noteId: node.data.noteId}); this.triggerCommand("showImportDialog", {noteId: node.data.noteId});
} }
forceNoteSyncCommand({node}) {
syncService.forceNoteSync(node.data.noteId);
}
editNoteTitleCommand({node}) { editNoteTitleCommand({node}) {
appContext.triggerCommand('focusOnTitle'); appContext.triggerCommand('focusOnTitle');
} }

View File

@ -3,7 +3,7 @@
const options = require('../../services/options'); const options = require('../../services/options');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const instanceId = require('../../services/member_id'); const instanceId = require('../../services/instance_id');
const passwordEncryptionService = require('../../services/encryption/password_encryption'); const passwordEncryptionService = require('../../services/encryption/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');

View File

@ -9,10 +9,8 @@ const optionService = require('../../services/options');
const contentHashService = require('../../services/content_hash'); const contentHashService = require('../../services/content_hash');
const log = require('../../services/log'); const log = require('../../services/log');
const syncOptions = require('../../services/sync_options'); const syncOptions = require('../../services/sync_options');
const dateUtils = require('../../services/date_utils');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const ws = require('../../services/ws'); const ws = require('../../services/ws');
const becca = require("../../becca/becca");
async function testSync() { async function testSync() {
try { try {
@ -84,54 +82,14 @@ function forceFullSync() {
syncService.sync(); syncService.sync();
} }
function forceNoteSync(req) {
const noteId = req.params.noteId;
const note = becca.getNote(noteId);
const now = dateUtils.utcNowDateTime();
sql.execute(`UPDATE notes SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
entityChangesService.moveEntityChangeToTop('notes', noteId);
sql.execute(`UPDATE blobs SET utcDateModified = ? WHERE blobId = ?`, [now, note.blobId]);
entityChangesService.moveEntityChangeToTop('blobs', note.blobId);
for (const branchId of sql.getColumn("SELECT branchId FROM branches WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE branches SET utcDateModified = ? WHERE branchId = ?`, [now, branchId]);
entityChangesService.moveEntityChangeToTop('branches', branchId);
}
for (const attributeId of sql.getColumn("SELECT attributeId FROM attributes WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE attributes SET utcDateModified = ? WHERE attributeId = ?`, [now, attributeId]);
entityChangesService.moveEntityChangeToTop('attributes', attributeId);
}
for (const revisionId of sql.getColumn("SELECT revisionId FROM revisions WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE revisions SET utcDateModified = ? WHERE revisionId = ?`, [now, revisionId]);
entityChangesService.moveEntityChangeToTop('revisions', revisionId);
}
for (const attachmentId of sql.getColumn("SELECT attachmentId FROM attachments WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE attachments SET utcDateModified = ? WHERE attachmentId = ?`, [now, attachmentId]);
entityChangesService.moveEntityChangeToTop('attachments', attachmentId);
}
log.info(`Forcing note sync for ${noteId}`);
// not awaiting for the job to finish (will probably take a long time)
syncService.sync();
}
function getChanged(req) { function getChanged(req) {
const startTime = Date.now(); const startTime = Date.now();
let lastEntityChangeId = parseInt(req.query.lastEntityChangeId); let lastEntityChangeId = parseInt(req.query.lastEntityChangeId);
const clientinstanceId = req.query.instanceId; const clientInstanceId = req.query.instanceId;
let filteredEntityChanges = []; let filteredEntityChanges = [];
while (filteredEntityChanges.length === 0) { do {
const entityChanges = sql.getRows(` const entityChanges = sql.getRows(`
SELECT * SELECT *
FROM entity_changes FROM entity_changes
@ -144,20 +102,22 @@ function getChanged(req) {
break; break;
} }
filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientinstanceId); filteredEntityChanges = entityChanges.filter(ec => ec.instanceId !== clientInstanceId);
if (filteredEntityChanges.length === 0) { if (filteredEntityChanges.length === 0) {
lastEntityChangeId = entityChanges[entityChanges.length - 1].id; lastEntityChangeId = entityChanges[entityChanges.length - 1].id;
} }
} } while (filteredEntityChanges.length === 0);
const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges); const entityChangeRecords = syncService.getEntityChangeRecords(filteredEntityChanges);
if (entityChangeRecords.length > 0) { if (entityChangeRecords.length > 0) {
lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id; lastEntityChangeId = entityChangeRecords[entityChangeRecords.length - 1].entityChange.id;
log.info(`Returning ${entityChangeRecords.length} entity changes in ${Date.now() - startTime}ms`);
} }
const ret = { return {
entityChanges: entityChangeRecords, entityChanges: entityChangeRecords,
lastEntityChangeId, lastEntityChangeId,
outstandingPullCount: sql.getValue(` outstandingPullCount: sql.getValue(`
@ -165,14 +125,8 @@ function getChanged(req) {
FROM entity_changes FROM entity_changes
WHERE isSynced = 1 WHERE isSynced = 1
AND instanceId != ? AND instanceId != ?
AND id > ?`, [clientinstanceId, lastEntityChangeId]) AND id > ?`, [clientInstanceId, lastEntityChangeId])
}; };
if (ret.entityChanges.length > 0) {
log.info(`Returning ${ret.entityChanges.length} entity changes in ${Date.now() - startTime}ms`);
}
return ret;
} }
const partialRequests = {}; const partialRequests = {};
@ -194,12 +148,12 @@ function update(req) {
} }
if (!partialRequests[requestId]) { if (!partialRequests[requestId]) {
throw new Error(`Partial request ${requestId}, index ${pageIndex} of ${pageCount} of pages does not have expected record.`); throw new Error(`Partial request ${requestId}, page ${pageIndex + 1} of ${pageCount} of pages does not have expected record.`);
} }
partialRequests[requestId].payload += req.body; partialRequests[requestId].payload += req.body;
log.info(`Receiving partial request ${requestId}, page index ${pageIndex} out of ${pageCount} pages.`); log.info(`Receiving a partial request ${requestId}, page ${pageIndex + 1} out of ${pageCount} pages.`);
if (pageIndex !== pageCount - 1) { if (pageIndex !== pageCount - 1) {
return; return;
@ -212,9 +166,11 @@ function update(req) {
const {entities, instanceId} = body; const {entities, instanceId} = body;
sql.transactional(() => {
for (const {entityChange, entity} of entities) { for (const {entityChange, entity} of entities) {
syncUpdateService.updateEntity(entityChange, entity, instanceId); syncUpdateService.updateEntity(entityChange, entity, instanceId);
} }
});
} }
setInterval(() => { setInterval(() => {
@ -241,8 +197,7 @@ function queueSector(req) {
} }
function checkEntityChanges() { function checkEntityChanges() {
const consistencyChecks = require("../../services/consistency_checks"); require("../../services/consistency_checks").runEntityChangesChecks();
consistencyChecks.runEntityChangesChecks();
} }
module.exports = { module.exports = {
@ -251,7 +206,6 @@ module.exports = {
syncNow, syncNow,
fillEntityChanges, fillEntityChanges,
forceFullSync, forceFullSync,
forceNoteSync,
getChanged, getChanged,
update, update,
getStats, getStats,

View File

@ -216,7 +216,6 @@ function register(app) {
apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow); apiRoute(PST, '/api/sync/now', syncApiRoute.syncNow);
apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges); apiRoute(PST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges);
apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync); apiRoute(PST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(PST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler); route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler);
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler); route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler); route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);

View File

@ -597,14 +597,10 @@ class ConsistencyChecks {
runEntityChangeChecks(entityName, key) { runEntityChangeChecks(entityName, key) {
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT ${key} as entityId
${key} as entityId FROM ${entityName}
FROM LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key}
${entityName} WHERE ec.id IS NULL`,
LEFT JOIN entity_changes ON entity_changes.entityName = '${entityName}'
AND entity_changes.entityId = ${key}
WHERE
entity_changes.id IS NULL`,
({entityId}) => { ({entityId}) => {
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
@ -613,7 +609,7 @@ class ConsistencyChecks {
entityName, entityName,
entityId, entityId,
hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK hash: utils.randomString(10), // doesn't matter, will force sync, but that's OK
isErased: !!entityRow.isErased, isErased: false,
utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated, utcDateChanged: entityRow.utcDateModified || entityRow.utcDateCreated,
isSynced: entityName !== 'options' || entityRow.isSynced isSynced: entityName !== 'options' || entityRow.isSynced
}); });
@ -625,15 +621,13 @@ class ConsistencyChecks {
}); });
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT id, entityId
id, entityId FROM entity_changes
FROM LEFT JOIN ${entityName} ON entityId = ${entityName}.${key}
entity_changes
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE WHERE
entity_changes.isErased = 0 entity_changes.isErased = 0
AND entity_changes.entityName = '${entityName}' AND entity_changes.entityName = '${entityName}'
AND ${key} IS NULL`, AND ${entityName}.${key} IS NULL`,
({id, entityId}) => { ({id, entityId}) => {
if (this.autoFix) { if (this.autoFix) {
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]); sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
@ -645,11 +639,9 @@ class ConsistencyChecks {
}); });
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT id, entityId
id, entityId FROM entity_changes
FROM JOIN ${entityName} ON entityId = ${entityName}.${key}
entity_changes
JOIN ${entityName} ON entityId = ${key}
WHERE WHERE
entity_changes.isErased = 1 entity_changes.isErased = 1
AND entity_changes.entityName = '${entityName}'`, AND entity_changes.entityName = '${entityName}'`,

View File

@ -14,7 +14,8 @@ function getEntityHashes() {
const hashRows = sql.getRawRows(` const hashRows = sql.getRawRows(`
SELECT entityName, SELECT entityName,
entityId, entityId,
hash hash,
isErased
FROM entity_changes FROM entity_changes
WHERE isSynced = 1 WHERE isSynced = 1
AND entityName != 'note_reordering'`); AND entityName != 'note_reordering'`);
@ -25,12 +26,17 @@ function getEntityHashes() {
const hashMap = {}; const hashMap = {};
for (const [entityName, entityId, hash] of hashRows) { for (const [entityName, entityId, hash, isErased] of hashRows) {
const entityHashMap = hashMap[entityName] = hashMap[entityName] || {}; const entityHashMap = hashMap[entityName] = hashMap[entityName] || {};
const sector = entityId[0]; const sector = entityId[0];
entityHashMap[sector] = (entityHashMap[sector] || "") + hash if (entityName === 'revisions' && sector === '5') {
console.log(entityId, hash, isErased);
}
// if the entity is erased, its hash is not updated, so it has to be added extra
entityHashMap[sector] = (entityHashMap[sector] || "") + hash + isErased;
} }
for (const entityHashMap of Object.values(hashMap)) { for (const entityHashMap of Object.values(hashMap)) {

View File

@ -3,7 +3,7 @@ const dateUtils = require('./date_utils');
const log = require('./log'); const log = require('./log');
const cls = require('./cls'); const cls = require('./cls');
const utils = require('./utils'); const utils = require('./utils');
const instanceId = require('./member_id'); const instanceId = require('./instance_id');
const becca = require("../becca/becca"); const becca = require("../becca/becca");
const blobService = require("../services/blob"); const blobService = require("../services/blob");
@ -62,8 +62,6 @@ function moveEntityChangeToTop(entityName, entityId) {
} }
function addEntityChangesForSector(entityName, sector) { function addEntityChangesForSector(entityName, sector) {
const startTime = Date.now();
const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]); const entityChanges = sql.getRows(`SELECT * FROM entity_changes WHERE entityName = ? AND SUBSTR(entityId, 1, 1) = ?`, [entityName, sector]);
sql.transactional(() => { sql.transactional(() => {
@ -72,7 +70,7 @@ function addEntityChangesForSector(entityName, sector) {
} }
}); });
log.info(`Added sector ${sector} of '${entityName}' (${entityChanges.length} entities) to sync queue in ${Date.now() - startTime}ms.`); log.info(`Added sector ${sector} of '${entityName}' (${entityChanges.length} entities) to the sync queue.`);
} }
function cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey) { function cleanupEntityChangesForMissingEntities(entityName, entityPrimaryKey) {
@ -103,39 +101,34 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') {
createdCount++; createdCount++;
let hash; const ec = {
let utcDateChanged; entityName,
let isSynced; entityId,
isErased: false
};
if (entityName === 'blobs') { if (entityName === 'blobs') {
const blob = sql.getRow("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]); const blob = sql.getRow("SELECT blobId, content, utcDateModified FROM blobs WHERE blobId = ?", [entityId]);
hash = blobService.calculateContentHash(blob); ec.hash = blobService.calculateContentHash(blob);
utcDateChanged = blob.utcDateModified; ec.utcDateChanged = blob.utcDateModified;
isSynced = true; // blobs are always synced ec.isSynced = true; // blobs are always synced
} else { } else {
const entity = becca.getEntity(entityName, entityId); const entity = becca.getEntity(entityName, entityId);
if (entity) { if (entity) {
hash = entity?.generateHash() || "|deleted"; ec.hash = entity.generateHash() || "|deleted";
utcDateChanged = entity?.getUtcDateChanged() || dateUtils.utcNowDateTime(); ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
isSynced = entityName !== 'options' || !!entity?.isSynced; ec.isSynced = entityName !== 'options' || !!entity.isSynced;
} else { } else {
// entity might be null (not present in becca) when it's deleted // entity might be null (not present in becca) when it's deleted
// FIXME: hacky, not sure if it might cause some problems // FIXME: hacky, not sure if it might cause some problems
hash = "deleted"; ec.hash = "deleted";
utcDateChanged = dateUtils.utcNowDateTime(); ec.utcDateChanged = dateUtils.utcNowDateTime();
isSynced = true; // deletable (the ones with isDeleted) entities are synced ec.isSynced = true; // deletable (the ones with isDeleted) entities are synced
} }
} }
addEntityChange({ addEntityChange(ec);
entityName,
entityId,
hash: hash,
isErased: false,
utcDateChanged: utcDateChanged,
isSynced: isSynced
});
} }
if (createdCount > 0) { if (createdCount > 0) {

View File

@ -37,6 +37,7 @@ function eraseNotes(noteIdsToErase) {
function setEntityChangesAsErased(entityChanges) { function setEntityChangesAsErased(entityChanges) {
for (const ec of entityChanges) { for (const ec of entityChanges) {
ec.isErased = true; ec.isErased = true;
ec.utcDateChanged = dateUtils.utcNowDateTime();
entityChangesService.addEntityChange(ec); entityChangesService.addEntityChange(ec);
} }

View File

@ -3,6 +3,7 @@
const log = require('./log'); const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const protectedSessionService = require("./protected_session"); const protectedSessionService = require("./protected_session");
const dateUtils = require("./date_utils");
/** /**
* @param {BNote} note * @param {BNote} note
@ -40,7 +41,7 @@ function eraseRevisions(revisionIdsToErase) {
log.info(`Removing note revisions: ${JSON.stringify(revisionIdsToErase)}`); log.info(`Removing note revisions: ${JSON.stringify(revisionIdsToErase)}`);
sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase); sql.executeMany(`DELETE FROM revisions WHERE revisionId IN (???)`, revisionIdsToErase);
sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase); sql.executeMany(`UPDATE entity_changes SET isErased = 1, utcDateChanged = '${dateUtils.utcNowDateTime()}' WHERE entityName = 'revisions' AND entityId IN (???)`, revisionIdsToErase);
} }
module.exports = { module.exports = {

View File

@ -4,7 +4,7 @@ const log = require('./log');
const sql = require('./sql'); const sql = require('./sql');
const optionService = require('./options'); const optionService = require('./options');
const utils = require('./utils'); const utils = require('./utils');
const instanceId = require('./member_id'); const instanceId = require('./instance_id');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncUpdateService = require('./sync_update'); const syncUpdateService = require('./sync_update');
const contentHashService = require('./content_hash'); const contentHashService = require('./content_hash');
@ -54,6 +54,7 @@ async function sync() {
}); });
} }
catch (e) { catch (e) {
// we're dynamically switching whether we're using proxy or not based on whether we encountered error with the current method
proxyToggle = !proxyToggle; proxyToggle = !proxyToggle;
if (e.message?.includes('ECONNREFUSED') || if (e.message?.includes('ECONNREFUSED') ||
@ -107,7 +108,7 @@ async function doLogin() {
}); });
if (resp.instanceId === instanceId) { if (resp.instanceId === instanceId) {
throw new Error(`Sync server has member ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`); throw new Error(`Sync server has instance ID '${resp.instanceId}' which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`);
} }
syncContext.instanceId = resp.instanceId; syncContext.instanceId = resp.instanceId;
@ -253,7 +254,7 @@ async function checkContentHash(syncContext) {
const failedChecks = contentHashService.checkContentHashes(resp.entityHashes); const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
if (failedChecks.length > 0) { if (failedChecks.length > 0) {
// before requeuing sectors, make sure the entity changes are correct // before re-queuing sectors, make sure the entity changes are correct
const consistencyChecks = require("./consistency_checks"); const consistencyChecks = require("./consistency_checks");
consistencyChecks.runEntityChangesChecks(); consistencyChecks.runEntityChangesChecks();
@ -350,7 +351,8 @@ function getEntityChangeRecords(entityChanges) {
length += JSON.stringify(record).length; length += JSON.stringify(record).length;
if (length > 1000000) { if (length > 1_000_000) {
// each sync request/response should have at most ~1 MB.
break; break;
} }
} }

View File

@ -4,98 +4,83 @@ const entityChangesService = require('./entity_changes');
const eventService = require('./events'); const eventService = require('./events');
const entityConstructor = require("../becca/entity_constructor"); const entityConstructor = require("../becca/entity_constructor");
function updateEntity(entityChange, entityRow, instanceId) { function updateEntity(remoteEC, remoteEntityRow, instanceId) {
// can be undefined for options with isSynced=false if (!remoteEntityRow && remoteEC.entityName === 'options') {
if (!entityRow) { return; // can be undefined for options with isSynced=false
if (entityChange.isSynced) {
if (entityChange.isErased) {
eraseEntity(entityChange, instanceId);
}
else {
log.info(`Encountered synced non-erased entity change without entity: ${JSON.stringify(entityChange)}`);
}
}
else if (entityChange.entityName !== 'options') {
log.info(`Encountered unsynced non-option entity change without entity: ${JSON.stringify(entityChange)}`);
} }
return; const updated = remoteEC.entityName === 'note_reordering'
} ? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId);
const updated = entityChange.entityName === 'note_reordering'
? updateNoteReordering(entityChange, entityRow, instanceId)
: updateNormalEntity(entityChange, entityRow, instanceId);
if (updated) { if (updated) {
if (entityRow.isDeleted) { if (remoteEntityRow?.isDeleted) {
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { eventService.emit(eventService.ENTITY_DELETE_SYNCED, {
entityName: entityChange.entityName, entityName: remoteEC.entityName,
entityId: entityChange.entityId entityId: remoteEC.entityId
}); });
} }
else if (!entityChange.isErased) { else if (!remoteEC.isErased) {
eventService.emit(eventService.ENTITY_CHANGE_SYNCED, { eventService.emit(eventService.ENTITY_CHANGE_SYNCED, {
entityName: entityChange.entityName, entityName: remoteEC.entityName,
entityRow entityRow: remoteEntityRow
}); });
} }
} }
} }
function updateNormalEntity(remoteEntityChange, remoteEntityRow, instanceId) { function updateNormalEntity(remoteEC, remoteEntityRow, instanceId) {
const localEntityChange = sql.getRow(` const localEC = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
SELECT utcDateChanged, hash, isErased
FROM entity_changes
WHERE entityName = ? AND entityId = ?`, [remoteEntityChange.entityName, remoteEntityChange.entityId]);
if (localEntityChange && !localEntityChange.isErased && remoteEntityChange.isErased) { if (!localEC?.isErased && remoteEC.isErased) {
sql.transactional(() => { eraseEntity(remoteEC, instanceId);
const primaryKey = entityConstructor.getEntityFromEntityName(remoteEntityChange.entityName).primaryKeyName;
sql.execute(`DELETE FROM ${remoteEntityChange.entityName} WHERE ${primaryKey} = ?`, remoteEntityChange.entityId);
entityChangesService.addEntityChangeWithInstanceId(remoteEntityChange, instanceId);
});
return true; return true;
} else if (localEC?.isErased && !remoteEC.isErased) {
// on this side, we can't unerase the entity, so force the entity to be erased on the other side.
entityChangesService.addEntityChangeWithInstanceId(localEC, null);
return false;
} }
if (!localEntityChange if (!localEC
|| localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged || localEC.utcDateChanged < remoteEC.utcDateChanged
|| localEntityChange.hash !== remoteEntityChange.hash // sync error, we should still update || (localEC.utcDateChanged === remoteEC.utcDateChanged && localEC.hash !== remoteEC.hash) // sync error, we should still update
) { ) {
if (remoteEntityChange.entityName === 'blobs') { 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 // 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 // "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) // is string note or note (syncs can arrive out of order)
remoteEntityRow.content = remoteEntityRow.content === null ? null : Buffer.from(remoteEntityRow.content, 'base64'); remoteEntityRow.content = Buffer.from(remoteEntityRow.content, 'base64');
if (remoteEntityRow.content?.byteLength === 0) { 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 // 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 = ""; remoteEntityRow.content = "";
} }
} }
sql.transactional(() => { sql.replace(remoteEC.entityName, remoteEntityRow);
sql.replace(remoteEntityChange.entityName, remoteEntityRow);
entityChangesService.addEntityChangeWithInstanceId(remoteEntityChange, instanceId); entityChangesService.addEntityChangeWithInstanceId(remoteEC, instanceId);
});
return true; return true;
} else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) {
// the change on our side is newer than on the other side, so the other side should update
entityChangesService.addEntityChangeWithInstanceId(localEC, null);
return false;
} }
return false; return false;
} }
function updateNoteReordering(entityChange, entity, instanceId) { function updateNoteReordering(remoteEC, remoteEntityRow, instanceId) {
sql.transactional(() => { for (const key in remoteEntityRow) {
for (const key in entity) { sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remoteEntityRow[key], key]);
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
} }
entityChangesService.addEntityChangeWithInstanceId(entityChange, instanceId); entityChangesService.addEntityChangeWithInstanceId(remoteEC, instanceId);
});
return true; return true;
} }
@ -109,19 +94,17 @@ function eraseEntity(entityChange, instanceId) {
"attributes", "attributes",
"revisions", "revisions",
"attachments", "attachments",
"blobs", "blobs"
]; ];
if (!entityNames.includes(entityName)) { if (!entityNames.includes(entityName)) {
log.error(`Cannot erase entity '${entityName}', id '${entityId}'`); log.error(`Cannot erase entity '${entityName}', id '${entityId}'.`);
return; return;
} }
const keyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; const primaryKeyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
sql.execute(`DELETE FROM ${entityName} WHERE ${keyName} = ?`, [entityId]); sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
eventService.emit(eventService.ENTITY_DELETE_SYNCED, { entityName, entityId });
entityChangesService.addEntityChangeWithInstanceId(entityChange, instanceId); entityChangesService.addEntityChangeWithInstanceId(entityChange, instanceId);
} }