2023-01-03 13:35:10 +01:00
import FBranch from "../entities/fbranch.js" ;
import FNote from "../entities/fnote.js" ;
import FAttribute from "../entities/fattribute.js" ;
2020-01-25 13:46:55 +01:00
import server from "./server.js" ;
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js" ;
2023-05-05 16:37:39 +02:00
import FBlob from "../entities/fblob.js" ;
2023-04-17 23:21:28 +02:00
import FAttachment from "../entities/fattachment.js" ;
2018-03-25 12:29:00 -04:00
2019-04-14 18:32:56 +02:00
/ * *
2021-10-16 22:13:34 +02:00
* Froca ( FROntend CAche ) 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 .
2021-10-16 22:13:34 +02:00
*
* Backend has a similar cache called Becca
2019-04-14 18:32:56 +02:00
* /
2021-04-16 22:57:37 +02:00
class Froca {
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
2023-05-05 23:41:11 +02:00
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
2020-02-01 22:29:32 +01:00
2023-01-03 13:35:10 +01:00
/** @type {Object.<string, FNote>} */
2018-03-25 12:29:00 -04:00
this . notes = { } ;
2018-04-16 20:40:18 -04:00
2023-01-03 13:35:10 +01:00
/** @type {Object.<string, FBranch>} */
2018-04-16 20:40:18 -04:00
this . branches = { } ;
2020-01-25 11:52:45 +01:00
2023-01-03 13:35:10 +01:00
/** @type {Object.<string, FAttribute>} */
2020-01-25 11:52:45 +01:00
this . attributes = { } ;
2020-02-01 18:29:18 +01:00
2023-04-03 23:47:24 +02:00
/** @type {Object.<string, FAttachment>} */
this . attachments = { } ;
2023-05-05 16:37:39 +02:00
/** @type {Object.<string, Promise<FBlob>>} */
2023-03-15 22:44:08 +01:00
this . blobPromises = { } ;
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 ) {
2022-12-21 15:19:05 +01:00
const resp = await server . get ( ` tree?subTreeNoteId= ${ subTreeNoteId } ` ) ;
2020-08-26 16:50:16 +02:00
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
2023-05-05 23:41:11 +02:00
// search note doesn't have child branches in the database and all the children are virtual branches
2020-10-20 22:33:38 +02:00
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 = [ ] ;
2020-12-17 21:19:52 +01:00
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 {
2023-01-03 13:35:10 +01:00
this . notes [ noteId ] = new FNote ( this , noteRow ) ;
2020-10-20 22:33:38 +02:00
}
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 ) {
2023-01-03 13:35:10 +01:00
const branch = new FBranch ( 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 ) {
2023-02-28 23:23:17 +01:00
childNote . addParent ( branch . parentNoteId , branch . branchId , false ) ;
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 ;
2023-01-03 13:35:10 +01:00
this . attributes [ attributeId ] = new FAttribute ( this , attributeRow ) ;
2020-01-25 11:52:45 +01:00
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 ( ) ;
2023-02-28 23:23:17 +01:00
this . notes [ noteId ] . sortParents ( ) ;
2020-12-10 16:10:10 +01:00
}
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
2021-03-18 21:05:43 +01:00
appContext . triggerEvent ( 'notesReloaded' , { noteIds } ) ;
}
2021-01-28 21:19:01 +01:00
2021-03-18 21:05:43 +01:00
async loadSearchNote ( noteId ) {
const note = await this . getNote ( noteId ) ;
2019-11-16 19:07:32 +01:00
2021-03-18 21:05:43 +01:00
if ( ! note || note . type !== 'search' ) {
return ;
}
2020-10-29 22:41:33 +01:00
2022-12-21 15:19:05 +01:00
const { searchResultNoteIds , highlightedTokens , error } = await server . get ( ` search-note/ ${ note . noteId } ` ) ;
2021-02-19 22:15:56 +01:00
2021-03-18 21:05:43 +01:00
if ( ! Array . isArray ( searchResultNoteIds ) ) {
2022-04-19 23:36:21 +02:00
throw new Error ( ` Search note ' ${ note . noteId } ' failed: ${ searchResultNoteIds } ` ) ;
2019-11-04 20:20:21 +01:00
}
2020-11-26 23:00:27 +01:00
2021-03-18 21:05:43 +01:00
// reset all the virtual branches from old search results
2021-04-16 22:57:37 +02:00
if ( note . noteId in froca . notes ) {
froca . notes [ note . noteId ] . children = [ ] ;
froca . notes [ note . noteId ] . childToBranch = { } ;
2021-03-18 21:05:43 +01:00
}
2021-12-20 17:30:47 +01:00
const branches = [ ... note . getParentBranches ( ) , ... note . getChildBranches ( ) ] ;
2021-07-02 23:22:10 +02:00
2021-03-18 21:05:43 +01:00
searchResultNoteIds . forEach ( ( resultNoteId , index ) => branches . push ( {
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
2022-12-21 15:19:05 +01:00
branchId : ` virt- ${ note . noteId } - ${ resultNoteId } ` ,
2021-03-18 21:05:43 +01:00
noteId : resultNoteId ,
parentNoteId : note . noteId ,
notePosition : ( index + 1 ) * 10 ,
fromSearchNote : true
} ) ) ;
// update this note with standard (parent) branches + virtual (children) branches
this . addResp ( {
notes : [ note ] ,
branches ,
attributes : [ ]
} ) ;
2021-04-16 22:57:37 +02:00
froca . notes [ note . noteId ] . searchResultsLoaded = true ;
2022-07-10 15:52:02 +02:00
froca . notes [ note . noteId ] . highlightedTokens = highlightedTokens ;
2022-12-17 13:07:42 +01:00
return { error } ;
2019-04-13 22:56:45 +02:00
}
2023-01-03 13:35:10 +01:00
/** @returns {FNote[]} */
2020-03-18 22:35:54 +01:00
getNotesFromCache ( noteIds , silentNotFoundError = false ) {
return noteIds . map ( noteId => {
if ( ! this . notes [ noteId ] && ! silentNotFoundError ) {
2023-01-03 21:30:49 +01: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 ) ;
}
2023-01-03 13:35:10 +01:00
/** @returns {Promise<FNote[]>} */
2018-08-16 23:00:04 +02:00
async getNotes ( noteIds , silentNotFoundError = false ) {
2022-12-13 21:45:57 +01:00
noteIds = Array . from ( new Set ( noteIds ) ) ; // make unique
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 ) {
2023-01-03 21:30:49 +01: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
}
2021-10-24 14:53:45 +02:00
/** @returns {Promise<boolean>} */
2019-04-13 22:10:16 +02:00
async noteExists ( noteId ) {
const notes = await this . getNotes ( [ noteId ] , true ) ;
return notes . length === 1 ;
}
2023-01-03 13:35:10 +01:00
/** @returns {Promise<FNote>} */
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 ) {
2022-04-19 23:36:21 +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
}
2023-01-03 13:35:10 +01:00
/** @returns {FNote|null} */
2020-01-15 21:36:01 +01:00
getNoteFromCache ( noteId ) {
2020-12-13 20:13:57 +01:00
if ( ! noteId ) {
throw new Error ( "Empty noteId" ) ;
}
2020-01-15 21:36:01 +01:00
return this . notes [ noteId ] ;
}
2023-01-03 13:35:10 +01:00
/** @returns {FBranch[]} */
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
2023-01-03 13:35:10 +01:00
/** @returns {FBranch} */
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 ) {
2023-01-03 21:30:49 +01: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' ) {
2023-01-03 21:30:49 +01:00
return 'none_root' ;
2020-09-19 22:47:14 +02:00
}
2020-01-21 21:43:23 +01:00
const child = await this . getNote ( childNoteId ) ;
2020-07-26 22:58:22 +02:00
if ( ! child ) {
2023-01-03 21:30:49 +01: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
2023-05-29 00:19:54 +02:00
/** @returns {Promise<FAttachment>} */
2023-06-30 15:25:45 +02:00
async getAttachment ( attachmentId , silentNotFoundError = false ) {
2023-05-29 00:19:54 +02:00
const attachment = this . attachments [ attachmentId ] ;
if ( attachment ) {
return attachment ;
}
// load all attachments for the given note even if one is requested, don't load one by one
2023-06-30 15:25:45 +02:00
let attachmentRows ;
try {
2023-06-30 15:44:30 +02:00
attachmentRows = await server . getWithSilentNotFound ( ` attachments/ ${ attachmentId } /all ` ) ;
2023-06-30 15:25:45 +02:00
}
catch ( e ) {
if ( silentNotFoundError ) {
logInfo ( ` Attachment ' ${ attachmentId } not found, but silentNotFoundError is enabled: ` + e . message ) ;
2023-06-30 15:44:30 +02:00
return null ;
2023-06-30 15:25:45 +02:00
} else {
throw e ;
}
}
2023-05-29 00:19:54 +02:00
const attachments = this . processAttachmentRows ( attachmentRows ) ;
if ( attachments . length ) {
attachments [ 0 ] . getNote ( ) . attachments = attachments ;
}
2023-04-17 23:21:28 +02:00
2023-05-29 00:19:54 +02:00
return this . attachments [ attachmentId ] ;
}
/** @returns {Promise<FAttachment[]>} */
async getAttachmentsForNote ( noteId ) {
const attachmentRows = await server . get ( ` notes/ ${ noteId } /attachments ` ) ;
return this . processAttachmentRows ( attachmentRows ) ;
}
/** @returns {FAttachment[]} */
processAttachmentRows ( attachmentRows ) {
return attachmentRows . map ( attachmentRow => {
let attachment ;
if ( attachmentRow . attachmentId in this . attachments ) {
attachment = this . attachments [ attachmentRow . attachmentId ] ;
attachment . update ( attachmentRow ) ;
} else {
attachment = new FAttachment ( this , attachmentRow ) ;
this . attachments [ attachment . attachmentId ] = attachment ;
}
return attachment ;
} ) ;
2023-04-17 23:21:28 +02:00
}
2023-05-05 22:21:51 +02:00
/** @returns {Promise<FBlob>} */
2023-05-05 16:37:39 +02:00
async getBlob ( entityType , entityId , opts = { } ) {
2023-05-05 22:21:51 +02:00
opts . preview = ! ! opts . preview ;
const key = ` ${ entityType } - ${ entityId } - ${ opts . preview } ` ;
2023-05-05 16:37:39 +02:00
if ( ! this . blobPromises [ key ] ) {
2023-05-05 22:21:51 +02:00
this . blobPromises [ key ] = server . get ( ` ${ entityType } / ${ entityId } /blob?preview= ${ opts . preview } ` )
2023-05-05 16:37:39 +02:00
. then ( row => new FBlob ( row ) )
. catch ( e => console . error ( ` Cannot get blob for ${ entityType } ' ${ entityId } ' ` ) ) ;
2020-08-04 21:57:08 +02:00
2023-01-03 21:30:49 +01:00
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
2020-08-04 21:57:08 +02:00
// 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)
2023-06-14 22:21:22 +02:00
// if the blob is updated within the cache lifetime, it should be invalidated by froca_updater
2023-05-05 16:37:39 +02:00
this . blobPromises [ key ] . then (
( ) => setTimeout ( ( ) => this . blobPromises [ key ] = null , 1000 )
2020-08-04 21:57:08 +02:00
) ;
2020-02-01 18:29:18 +01:00
}
2023-05-05 16:37:39 +02:00
return await this . blobPromises [ key ] ;
2020-02-01 18:29:18 +01:00
}
2018-03-25 12:29:00 -04:00
}
2021-04-16 22:57:37 +02:00
const froca = new Froca ( ) ;
2018-03-25 12:29:00 -04:00
2021-04-16 22:57:37 +02:00
export default froca ;