2018-03-25 13:02:39 -04:00
import Branch from "../entities/branch.js" ;
import NoteShort from "../entities/note_short.js" ;
2020-01-25 11:52:45 +01:00
import Attribute from "../entities/attribute.js" ;
2020-01-25 13:46:55 +01:00
import server from "./server.js" ;
2020-11-26 23:00:27 +01:00
import appContext from "./app_context.js" ;
2020-02-01 18:29:18 +01:00
import NoteComplement from "../entities/note_complement.js" ;
2018-03-25 12:29:00 -04:00
2019-04-14 18:32:56 +02:00
/ * *
* TreeCache keeps a read only cache of note tree structure in frontend ' s memory .
2019-10-27 19:17:32 +01:00
* - notes are loaded lazily when unknown noteId is requested
* - when note is loaded , all its parent and child branches are loaded as well . For a branch to be used , it ' s not must be loaded before
* - deleted notes are present in the cache as well , but they don ' t have any branches . As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded , we can infer it is deleted .
*
* Note and branch deletions are corner cases and usually not needed .
2019-04-14 18:32:56 +02:00
* /
2018-03-25 12:29:00 -04:00
class TreeCache {
2018-08-16 23:00:04 +02:00
constructor ( ) {
2020-02-01 22:29:32 +01:00
this . initializedPromise = this . loadInitialTree ( ) ;
2018-08-16 23:00:04 +02:00
}
2020-02-01 22:29:32 +01:00
async loadInitialTree ( ) {
2020-03-18 22:35:54 +01:00
const resp = await server . get ( 'tree' ) ;
2020-02-01 22:29:32 +01:00
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
2018-03-25 12:29:00 -04:00
/** @type {Object.<string, NoteShort>} */
this . notes = { } ;
2018-04-16 20:40:18 -04:00
/** @type {Object.<string, Branch>} */
this . branches = { } ;
2020-01-25 11:52:45 +01:00
/** @type {Object.<string, Attribute>} */
this . attributes = { } ;
2020-02-01 18:29:18 +01:00
/** @type {Object.<string, Promise<NoteComplement>>} */
this . noteComplementPromises = { } ;
2019-10-25 21:47:14 +02:00
2020-03-18 22:35:54 +01:00
this . addResp ( resp ) ;
2019-10-25 21:47:14 +02:00
}
2020-08-26 16:50:16 +02:00
async loadSubTree ( subTreeNoteId ) {
const resp = await server . get ( 'tree?subTreeNoteId=' + subTreeNoteId ) ;
this . addResp ( resp ) ;
return this . notes [ subTreeNoteId ] ;
}
2020-03-18 22:35:54 +01:00
addResp ( resp ) {
const noteRows = resp . notes ;
const branchRows = resp . branches ;
const attributeRows = resp . attributes ;
2020-12-10 16:10:10 +01:00
const noteIdsToSort = new Set ( ) ;
2019-10-26 09:51:08 +02:00
for ( const noteRow of noteRows ) {
const { noteId } = noteRow ;
2020-10-20 22:33:38 +02:00
let note = this . notes [ noteId ] ;
2019-10-26 09:51:08 +02:00
2020-10-20 22:33:38 +02:00
if ( note ) {
note . update ( noteRow ) ;
2019-10-26 09:51:08 +02:00
2020-10-20 22:33:38 +02:00
// search note doesn't have child branches in database and all the children are virtual branches
if ( note . type !== 'search' ) {
for ( const childNoteId of note . children ) {
const childNote = this . notes [ childNoteId ] ;
2019-10-26 09:51:08 +02:00
2020-10-20 22:33:38 +02:00
if ( childNote ) {
childNote . parents = childNote . parents . filter ( p => p !== noteId ) ;
delete this . branches [ childNote . parentToBranch [ noteId ] ] ;
delete childNote . parentToBranch [ noteId ] ;
}
2019-10-26 09:51:08 +02:00
}
2020-10-20 22:33:38 +02:00
note . children = [ ] ;
note . childToBranch = [ ] ;
2019-10-26 09:51:08 +02:00
}
2019-04-13 22:10:16 +02:00
2020-10-20 22:33:38 +02:00
// we want to remove all "real" branches (represented in the database) since those will be created
// from branches argument but want to preserve all virtual ones from saved search
note . parents = note . parents . filter ( parentNoteId => {
2019-10-26 09:51:08 +02:00
const parentNote = this . notes [ parentNoteId ] ;
2020-10-20 22:33:38 +02:00
const branch = this . branches [ parentNote . childToBranch [ noteId ] ] ;
2019-04-13 22:10:16 +02:00
2020-10-20 22:33:38 +02:00
if ( ! parentNote || ! branch ) {
return false ;
}
2019-04-13 22:10:16 +02:00
2020-10-20 22:33:38 +02:00
if ( branch . fromSearchNote ) {
return true ;
2019-10-26 09:51:08 +02:00
}
2019-04-13 22:10:16 +02:00
2020-10-20 22:33:38 +02:00
parentNote . children = parentNote . children . filter ( p => p !== noteId ) ;
2019-04-13 22:10:16 +02:00
2020-10-20 22:33:38 +02:00
delete this . branches [ parentNote . childToBranch [ noteId ] ] ;
delete parentNote . childToBranch [ noteId ] ;
return false ;
} ) ;
}
else {
this . notes [ noteId ] = new NoteShort ( this , noteRow ) ;
}
2020-01-31 20:52:31 +01:00
}
2019-04-13 22:10:16 +02:00
2020-01-31 20:52:31 +01:00
for ( const branchRow of branchRows ) {
const branch = new Branch ( this , branchRow ) ;
2019-04-13 22:10:16 +02:00
2020-01-31 20:52:31 +01:00
this . branches [ branch . branchId ] = branch ;
const childNote = this . notes [ branch . noteId ] ;
if ( childNote ) {
childNote . addParent ( branch . parentNoteId , branch . branchId ) ;
2019-10-26 09:51:08 +02:00
}
2019-04-13 22:56:45 +02:00
2020-01-31 20:52:31 +01:00
const parentNote = this . notes [ branch . parentNoteId ] ;
2019-04-13 22:56:45 +02:00
2020-01-31 20:52:31 +01:00
if ( parentNote ) {
2020-12-10 16:10:10 +01:00
parentNote . addChild ( branch . noteId , branch . branchId , false ) ;
noteIdsToSort . add ( parentNote . noteId ) ;
2019-10-26 09:51:08 +02:00
}
}
2020-01-25 11:52:45 +01:00
for ( const attributeRow of attributeRows ) {
const { attributeId } = attributeRow ;
this . attributes [ attributeId ] = new Attribute ( this , attributeRow ) ;
const note = this . notes [ attributeRow . noteId ] ;
2020-09-05 22:45:26 +02:00
if ( note && ! note . attributes . includes ( attributeId ) ) {
2020-01-25 11:52:45 +01:00
note . attributes . push ( attributeId ) ;
}
if ( attributeRow . type === 'relation' ) {
const targetNote = this . notes [ attributeRow . value ] ;
if ( targetNote ) {
2020-02-25 16:31:44 +01:00
if ( ! targetNote . targetRelations . includes ( attributeId ) ) {
targetNote . targetRelations . push ( attributeId ) ;
2020-01-25 11:52:45 +01:00
}
}
}
}
2020-12-10 16:10:10 +01:00
// sort all of them at once, this avoids repeated sorts (#1480)
for ( const noteId of noteIdsToSort ) {
this . notes [ noteId ] . sortChildren ( ) ;
}
2019-10-26 09:51:08 +02:00
}
2020-01-26 11:41:40 +01:00
async reloadNotes ( noteIds ) {
if ( noteIds . length === 0 ) {
return ;
}
2020-01-29 20:14:02 +01:00
noteIds = Array . from ( new Set ( noteIds ) ) ; // make noteIds unique
2019-10-26 09:51:08 +02:00
const resp = await server . post ( 'tree/load' , { noteIds } ) ;
2020-03-18 22:35:54 +01:00
this . addResp ( resp ) ;
2019-11-04 20:20:21 +01:00
2020-11-26 23:00:27 +01:00
const searchNoteIds = [ ] ;
2019-11-04 20:20:21 +01:00
for ( const note of resp . notes ) {
if ( note . type === 'search' ) {
2020-11-26 23:00:27 +01:00
searchNoteIds . push ( note . noteId ) ;
2020-08-20 15:23:24 +02:00
const searchResultNoteIds = await server . get ( 'search-note/' + note . noteId ) ;
2019-11-04 20:20:21 +01:00
2020-10-26 19:58:56 +01:00
if ( ! Array . isArray ( searchResultNoteIds ) ) {
throw new Error ( ` Search note ${ note . noteId } failed: ${ searchResultNoteIds } ` ) ;
2019-11-16 19:07:32 +01:00
}
2019-11-04 20:20:21 +01:00
// force to load all the notes at once instead of one by one
2020-08-20 15:23:24 +02:00
await this . getNotes ( searchResultNoteIds ) ;
2019-11-04 20:20:21 +01:00
2020-10-29 22:41:33 +01:00
// reset all the virtual branches from old search results
if ( note . noteId in treeCache . notes ) {
treeCache . notes [ note . noteId ] . children = [ ] ;
}
2019-11-04 20:20:21 +01:00
const branches = resp . branches . filter ( b => b . noteId === note . noteId || b . parentNoteId === note . noteId ) ;
2020-08-20 15:23:24 +02:00
searchResultNoteIds . forEach ( ( resultNoteId , index ) => branches . push ( {
2019-11-04 20:20:21 +01:00
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
2020-08-20 15:23:24 +02:00
branchId : "virt" + resultNoteId + '-' + note . noteId ,
noteId : resultNoteId ,
2019-11-04 20:20:21 +01:00
parentNoteId : note . noteId ,
2020-10-20 22:33:38 +02:00
notePosition : ( index + 1 ) * 10 ,
fromSearchNote : true
2019-11-04 20:20:21 +01:00
} ) ) ;
// update this note with standard (parent) branches + virtual (children) branches
2020-03-18 22:35:54 +01:00
this . addResp ( {
notes : [ note ] ,
branches ,
attributes : [ ]
} ) ;
2019-11-04 20:20:21 +01:00
}
}
2020-11-26 23:00:27 +01:00
if ( searchNoteIds . length > 0 ) {
appContext . triggerEvent ( 'searchResultsUpdated' , { searchNoteIds } ) ;
}
2019-04-13 22:56:45 +02:00
}
2020-03-18 22:35:54 +01:00
/** @return {NoteShort[]} */
getNotesFromCache ( noteIds , silentNotFoundError = false ) {
return noteIds . map ( noteId => {
if ( ! this . notes [ noteId ] && ! silentNotFoundError ) {
2020-08-02 22:53:57 +02:00
console . trace ( ` Can't find note " ${ noteId } " ` ) ;
2020-03-18 22:35:54 +01:00
return null ;
}
else {
return this . notes [ noteId ] ;
}
} ) . filter ( note => ! ! note ) ;
}
2019-04-13 22:10:16 +02:00
/** @return {Promise<NoteShort[]>} */
2018-08-16 23:00:04 +02:00
async getNotes ( noteIds , silentNotFoundError = false ) {
2019-12-16 22:00:44 +01:00
const missingNoteIds = noteIds . filter ( noteId => ! this . notes [ noteId ] ) ;
2018-04-16 20:40:18 -04:00
2020-01-26 11:41:40 +01:00
await this . reloadNotes ( missingNoteIds ) ;
2018-04-16 20:40:18 -04:00
2018-04-16 23:13:33 -04:00
return noteIds . map ( noteId => {
2018-08-16 23:00:04 +02:00
if ( ! this . notes [ noteId ] && ! silentNotFoundError ) {
2020-08-20 15:23:24 +02:00
console . trace ( ` Can't find note " ${ noteId } " ` ) ;
2018-08-06 11:30:37 +02:00
2018-08-12 12:59:38 +02:00
return null ;
2020-11-22 23:05:02 +01:00
} else {
2018-04-16 23:13:33 -04:00
return this . notes [ noteId ] ;
}
2019-11-17 10:24:06 +01:00
} ) . filter ( note => ! ! note ) ;
2018-04-16 23:13:33 -04:00
}
2019-04-13 22:10:16 +02:00
/** @return {Promise<boolean>} */
async noteExists ( noteId ) {
const notes = await this . getNotes ( [ noteId ] , true ) ;
return notes . length === 1 ;
}
/** @return {Promise<NoteShort>} */
2019-09-08 11:25:57 +02:00
async getNote ( noteId , silentNotFoundError = false ) {
2018-05-26 16:16:34 -04:00
if ( noteId === 'none' ) {
2020-05-03 13:52:12 +02:00
console . trace ( ` No 'none' note. ` ) ;
2019-12-10 21:31:24 +01:00
return null ;
}
else if ( ! noteId ) {
2020-07-23 23:38:38 +02:00
console . trace ( ` Falsy noteId ${ noteId } , returning null. ` ) ;
2018-05-26 16:16:34 -04:00
return null ;
}
2019-09-08 11:25:57 +02:00
return ( await this . getNotes ( [ noteId ] , silentNotFoundError ) ) [ 0 ] ;
2018-03-25 12:29:00 -04:00
}
2020-01-15 21:36:01 +01:00
getNoteFromCache ( noteId ) {
return this . notes [ noteId ] ;
}
2020-05-03 13:15:08 +02:00
getBranches ( branchIds , silentNotFoundError = false ) {
2019-10-26 09:58:00 +02:00
return branchIds
2020-05-03 13:15:08 +02:00
. map ( branchId => this . getBranch ( branchId , silentNotFoundError ) )
. filter ( b => ! ! b ) ;
2019-10-26 09:58:00 +02:00
}
2018-04-16 20:40:18 -04:00
2019-10-26 09:58:00 +02:00
/** @return {Branch} */
2019-10-26 22:50:46 +02:00
getBranch ( branchId , silentNotFoundError = false ) {
2019-10-26 09:58:00 +02:00
if ( ! ( branchId in this . branches ) ) {
2019-10-26 22:50:46 +02:00
if ( ! silentNotFoundError ) {
2020-10-12 21:05:34 +02:00
logError ( ` Not existing branch ${ branchId } ` ) ;
2019-10-26 22:50:46 +02:00
}
2018-04-16 20:40:18 -04:00
}
2019-10-26 20:48:56 +02:00
else {
return this . branches [ branchId ] ;
}
2018-03-25 12:29:00 -04:00
}
2020-01-21 21:43:23 +01:00
async getBranchId ( parentNoteId , childNoteId ) {
2020-09-19 22:47:14 +02:00
if ( childNoteId === 'root' ) {
return 'root' ;
}
2020-01-21 21:43:23 +01:00
const child = await this . getNote ( childNoteId ) ;
2020-07-26 22:58:22 +02:00
if ( ! child ) {
2020-10-12 21:05:34 +02:00
logError ( ` Could not find branchId for parent= ${ parentNoteId } , child= ${ childNoteId } since child does not exist ` ) ;
2020-07-26 22:58:22 +02:00
return null ;
}
2020-01-21 21:43:23 +01:00
return child . parentToBranch [ parentNoteId ] ;
}
2020-01-26 10:42:24 +01:00
2020-06-14 14:30:57 +02:00
/ * *
* @ return { Promise < NoteComplement > }
* /
2020-02-01 18:29:18 +01:00
async getNoteComplement ( noteId ) {
if ( ! this . noteComplementPromises [ noteId ] ) {
this . noteComplementPromises [ noteId ] = server . get ( 'notes/' + noteId ) . then ( row => new NoteComplement ( row ) ) ;
2020-08-04 21:57:08 +02:00
// we don't want to keep large payloads forever in memory so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
// this is also a work around for missing invalidation after change
this . noteComplementPromises [ noteId ] . then (
( ) => setTimeout ( ( ) => this . noteComplementPromises [ noteId ] = null , 1000 )
) ;
2020-02-01 18:29:18 +01:00
}
return await this . noteComplementPromises [ noteId ] ;
}
2018-03-25 12:29:00 -04:00
}
const treeCache = new TreeCache ( ) ;
2020-06-09 22:59:22 +02:00
export default treeCache ;