2024-07-18 21:35:17 +03:00
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" ;
2025-01-09 18:36:24 +02:00
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 ;
}
2024-07-21 21:14:27 +03:00
updateEntity ( entityChange , entity , instanceId , updateContext ) ;
2023-09-21 18:13:14 +02:00
}
logUpdateContext ( updateContext ) ;
}
2024-07-21 21:14:27 +03:00
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
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
} ) ;
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
}
2024-07-21 21:14:27 +03:00
function updateNormalEntity ( remoteEC : EntityChange , remoteEntityRow : EntityRow | undefined , instanceId : string , updateContext : UpdateContext ) {
2024-07-15 19:25:31 +03:00
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
2020-04-04 14:57:19 +02: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 ;
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 ) {
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
}
}
}
2024-07-21 21:14:27 +03: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 ) ;
2020-04-04 14:57:19 +02:00
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 ;
2021-11-12 21:19:23 +01:00
2025-03-12 18:22:05 +00:00
const entityNames = [ "notes" , "branches" , "attributes" , "revisions" , "attachments" , "blobs" , "note_embeddings" ] ;
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 ) {
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 ) ) ;
}
2024-07-18 21:42:44 +03:00
export default {
2023-09-21 18:13:14 +02:00
updateEntities
2020-06-20 12:31:38 +02:00
} ;