Notes/src/services/sync_update.ts

235 lines
8.8 KiB
TypeScript
Raw Normal View History

import sql from "./sql.js";
import log from "./log.js";
import entityChangesService from "./entity_changes.js";
import eventService from "./events.js";
import entityConstructor from "../becca/entity_constructor.js";
import ws from "./ws.js";
import type { EntityChange, EntityChangeRecord, EntityRow } from "./entity_changes_interface.js";
2024-02-18 12:40:30 +02:00
interface UpdateContext {
alreadyErased: number;
erased: number;
2025-01-09 18:07:02 +02:00
updated: Record<string, string[]>;
2024-02-18 12:40:30 +02:00
}
2024-02-18 13:10:51 +02:00
function updateEntities(entityChanges: EntityChangeRecord[], 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
};
2025-01-09 18:07:02 +02:00
for (const { entityChange, entity } of entityChanges) {
const changeAppliedAlready = entityChange.changeId && !!sql.getValue("SELECT 1 FROM entity_changes WHERE changeId = ?", [entityChange.changeId]);
2023-09-21 18:13:14 +02:00
if (changeAppliedAlready) {
updateContext.alreadyUpdated++;
continue;
}
2025-01-09 18:07:02 +02:00
if (!atLeastOnePullApplied) {
// avoid spamming and send only for first
2023-09-21 18:13:14 +02:00
ws.syncPullInProgress();
atLeastOnePullApplied = true;
}
updateEntity(entityChange, entity, instanceId, updateContext);
2023-09-21 18:13:14 +02:00
}
logUpdateContext(updateContext);
}
function updateEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
2025-01-09 18:07:02 +02:00
if (!remoteEntityRow && remoteEC.entityName === "options") {
2023-07-29 21:59:20 +02:00
return; // can be undefined for options with isSynced=false
2020-03-08 21:59:19 +01:00
}
2025-03-12 18:22:05 +00:00
const updated = remoteEC.entityName === "note_reordering"
? updateNoteReordering(remoteEC, remoteEntityRow, instanceId)
: (remoteEC.entityName === "note_embeddings"
? updateNoteEmbedding(remoteEC, remoteEntityRow, instanceId, updateContext)
: updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext));
2019-01-03 23:27:10 +01: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
});
2025-01-09 18:07:02 +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
}
function updateNormalEntity(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
const localEC = sql.getRow<EntityChange | undefined>(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [remoteEC.entityName, remoteEC.entityId]);
2025-01-09 18:07:02 +02:00
const localECIsOlderOrSameAsRemote = localEC && localEC.utcDateChanged && remoteEC.utcDateChanged && localEC.utcDateChanged <= remoteEC.utcDateChanged;
2020-12-14 13:58:02 +01:00
2024-07-15 19:31:59 +03:00
if (!localEC || localECIsOlderOrSameAsRemote) {
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
2025-01-09 18:07:02 +02:00
if (!localEC || localECIsOlderOrSameAsRemote || 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
return true;
2025-01-09 18:07:02 +02:00
} else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased) && !localECIsOlderOrSameAsRemote) {
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;
}
return false;
2019-11-01 20:00:56 +01:00
}
2024-02-18 12:40:30 +02:00
function preProcessContent(remoteEC: EntityChange, remoteEntityRow: EntityRow) {
2025-01-09 18:07:02 +02:00
if (remoteEC.entityName === "blobs" && remoteEntityRow.content !== null) {
2023-10-18 09:37:36 +02:00
// 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") {
2025-01-09 18:07:02 +02:00
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
}
}
}
function updateNoteReordering(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, 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);
return true;
2017-11-09 20:52:47 -05:00
}
2025-03-12 18:22:05 +00:00
function updateNoteEmbedding(remoteEC: EntityChange, remoteEntityRow: EntityRow | undefined, instanceId: string, updateContext: UpdateContext) {
if (remoteEC.isErased) {
eraseEntity(remoteEC);
updateContext.erased++;
return true;
}
if (!remoteEntityRow) {
log.error(`Entity ${remoteEC.entityName} ${remoteEC.entityId} not found in sync update.`);
return false;
}
interface NoteEmbeddingRow {
embedId: string;
noteId: string;
providerId: string;
modelId: string;
dimension: number;
embedding: Buffer;
version: number;
dateCreated: string;
utcDateCreated: string;
dateModified: string;
utcDateModified: string;
}
// Cast remoteEntityRow to include required embedding properties
const typedRemoteEntityRow = remoteEntityRow as unknown as NoteEmbeddingRow;
const localEntityRow = sql.getRow<NoteEmbeddingRow>(`SELECT * FROM note_embeddings WHERE embedId = ?`, [remoteEC.entityId]);
if (localEntityRow) {
// We already have this embedding, check if we need to update it
if (localEntityRow.utcDateModified >= typedRemoteEntityRow.utcDateModified) {
// Local is newer or same, no need to update
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
return true;
} else {
// Remote is newer, update local
sql.replace("note_embeddings", remoteEntityRow);
if (!updateContext.updated[remoteEC.entityName]) {
updateContext.updated[remoteEC.entityName] = [];
}
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
return true;
}
} else {
// We don't have this embedding, insert it
sql.replace("note_embeddings", remoteEntityRow);
if (!updateContext.updated[remoteEC.entityName]) {
updateContext.updated[remoteEC.entityName] = [];
}
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
return true;
}
}
2024-02-18 12:40:30 +02:00
function eraseEntity(entityChange: EntityChange) {
2025-01-09 18:07:02 +02:00
const { entityName, entityId } = entityChange;
2025-03-12 18:22:05 +00:00
const entityNames = ["notes", "branches", "attributes", "revisions", "attachments", "blobs", "note_embeddings"];
if (!entityNames.includes(entityName)) {
2023-12-30 00:34:46 +01:00
log.error(`Cannot erase ${entityName} '${entityId}'.`);
return;
}
2023-07-29 21:59:20 +02:00
const primaryKeyName = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
2023-07-29 21:59:20 +02:00
sql.execute(`DELETE FROM ${entityName} WHERE ${primaryKeyName} = ?`, [entityId]);
}
2024-02-18 12:40:30 +02:00
function logUpdateContext(updateContext: UpdateContext) {
2025-01-09 18:07:02 +02:00
const message = JSON.stringify(updateContext).replaceAll('"', "").replaceAll(":", ": ").replaceAll(",", ", ");
2023-09-21 18:13:14 +02:00
log.info(message.substr(1, message.length - 2));
}
export default {
2023-09-21 18:13:14 +02:00
updateEntities
2020-06-20 12:31:38 +02:00
};