mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	frontend attribute cache refactoring WIP
This commit is contained in:
		| @@ -15,12 +15,6 @@ class Attribute { | ||||
|         this.position = row.position; | ||||
|         /** @param {boolean} isInheritable */ | ||||
|         this.isInheritable = row.isInheritable; | ||||
|         /** @param {boolean} isDeleted */ | ||||
|         this.isDeleted = row.isDeleted; | ||||
|         /** @param {string} utcDateCreated */ | ||||
|         this.utcDateCreated = row.utcDateCreated; | ||||
|         /** @param {string} utcDateModified */ | ||||
|         this.utcDateModified = row.utcDateModified; | ||||
|     } | ||||
|  | ||||
|     /** @returns {NoteShort} */ | ||||
| @@ -29,7 +23,7 @@ class Attribute { | ||||
|     } | ||||
|  | ||||
|     get toString() { | ||||
|         return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name})`; | ||||
|         return `Attribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -31,12 +31,12 @@ class NoteShort { | ||||
|         this.mime = row.mime; | ||||
|         /** @param {boolean} */ | ||||
|         this.isDeleted = row.isDeleted; | ||||
|         /** @param {boolean} */ | ||||
|         this.archived = row.archived; | ||||
|         /** @param {string} */ | ||||
|         this.cssClass = row.cssClass; | ||||
|         /** @param {string} */ | ||||
|         this.iconClass = row.iconClass; | ||||
|  | ||||
|         /** @type {string[]} */ | ||||
|         this.attributes = []; | ||||
|  | ||||
|         /** @type {string[]} */ | ||||
|         this.targetRelations = []; | ||||
|  | ||||
|         /** @type {string[]} */ | ||||
|         this.parents = []; | ||||
| @@ -306,7 +306,7 @@ class NoteShort { | ||||
|      * Clear note's attributes cache to force fresh reload for next attribute request. | ||||
|      * Cache is note instance scoped. | ||||
|      */ | ||||
|     invalidate__attributeCache() { | ||||
|     invalidateAttributeCache() { | ||||
|         this.__attributeCache = null; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -49,10 +49,6 @@ ws.subscribeToOutsideSyncMessages(syncData => { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| ws.subscribeToAllSyncMessages(syncData => { | ||||
|     appContext.trigger('syncData', {data: syncData}); | ||||
| }); | ||||
|  | ||||
| function noteChanged() { | ||||
|     const activeTabContext = appContext.getActiveTabContext(); | ||||
|  | ||||
|   | ||||
| @@ -455,32 +455,7 @@ ws.subscribeToMessages(message => { | ||||
|    } | ||||
| }); | ||||
|  | ||||
| // this is a synchronous handler - it returns only once the data has been updated | ||||
| ws.subscribeToOutsideSyncMessages(async syncData => { | ||||
|     const noteIdsToRefresh = new Set(); | ||||
|  | ||||
|     // this has the problem that the former parentNoteId might not be invalidated | ||||
|     // and the former location of the branch/note won't be removed. | ||||
|     syncData.filter(sync => sync.entityName === 'branches').forEach(sync => noteIdsToRefresh.add(sync.parentNoteId)); | ||||
|  | ||||
|     syncData.filter(sync => sync.entityName === 'notes').forEach(sync => noteIdsToRefresh.add(sync.entityId)); | ||||
|  | ||||
|     syncData.filter(sync => sync.entityName === 'note_reordering').forEach(sync => noteIdsToRefresh.add(sync.entityId)); | ||||
|  | ||||
|     syncData.filter(sync => sync.entityName === 'attributes').forEach(sync => { | ||||
|         const note = treeCache.notes[sync.noteId]; | ||||
|  | ||||
|         if (note && note.__attributeCache) { | ||||
|             noteIdsToRefresh.add(sync.entityId); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     if (noteIdsToRefresh.size > 0) { | ||||
|         appContext.trigger('reloadNotes', {noteIds: Array.from(noteIdsToRefresh)}); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| $(window).bind('hashchange', async function() { | ||||
| $(window).on('hashchange', function() { | ||||
|     if (isNotePathInAddress()) { | ||||
|         const [notePath, tabId] = getHashValueFromAddress(); | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import Branch from "../entities/branch.js"; | ||||
| import NoteShort from "../entities/note_short.js"; | ||||
| import ws from "./ws.js"; | ||||
| import server from "./server.js"; | ||||
| import Attribute from "../entities/attribute.js"; | ||||
|  | ||||
| /** | ||||
|  * TreeCache keeps a read only cache of note tree structure in frontend's memory. | ||||
| @@ -22,15 +23,18 @@ class TreeCache { | ||||
|  | ||||
|         /** @type {Object.<string, Branch>} */ | ||||
|         this.branches = {}; | ||||
|  | ||||
|         /** @type {Object.<string, Attribute>} */ | ||||
|         this.attributes = {}; | ||||
|     } | ||||
|  | ||||
|     load(noteRows, branchRows) { | ||||
|     load(noteRows, branchRows, attributeRows) { | ||||
|         this.init(); | ||||
|  | ||||
|         this.addResp(noteRows, branchRows); | ||||
|         this.addResp(noteRows, branchRows, attributeRows); | ||||
|     } | ||||
|  | ||||
|     addResp(noteRows, branchRows) { | ||||
|     addResp(noteRows, branchRows, attributeRows) { | ||||
|         const branchesByNotes = {}; | ||||
|  | ||||
|         for (const branchRow of branchRows) { | ||||
| @@ -96,6 +100,28 @@ class TreeCache { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (const attributeRow of attributeRows) { | ||||
|             const {attributeId} = attributeRow; | ||||
|  | ||||
|             this.attributes[attributeId] = new Attribute(this, attributeRow); | ||||
|  | ||||
|             const note = this.notes[attributeRow.noteId]; | ||||
|  | ||||
|             if (!note.attributes.includes(attributeId)) { | ||||
|                 note.attributes.push(attributeId); | ||||
|             } | ||||
|  | ||||
|             if (attributeRow.type === 'relation') { | ||||
|                 const targetNote = this.notes[attributeRow.value]; | ||||
|  | ||||
|                 if (targetNote) { | ||||
|                     if (!note.targetRelations.includes(attributeId)) { | ||||
|                         note.targetRelations.push(attributeId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async reloadNotes(noteIds) { | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import utils from './utils.js'; | ||||
| import toastService from "./toast.js"; | ||||
| import server from "./server.js"; | ||||
| import appContext from "./app_context.js"; | ||||
|  | ||||
| const $outstandingSyncsCount = $("#outstanding-syncs-count"); | ||||
|  | ||||
| const allSyncMessageHandlers = []; | ||||
| const outsideSyncMessageHandlers = []; | ||||
| const messageHandlers = []; | ||||
|  | ||||
| @@ -34,10 +34,6 @@ function subscribeToOutsideSyncMessages(messageHandler) { | ||||
|     outsideSyncMessageHandlers.push(messageHandler); | ||||
| } | ||||
|  | ||||
| function subscribeToAllSyncMessages(messageHandler) { | ||||
|     allSyncMessageHandlers.push(messageHandler); | ||||
| } | ||||
|  | ||||
| // used to serialize sync operations | ||||
| let consumeQueuePromise = null; | ||||
|  | ||||
| @@ -139,7 +135,7 @@ async function consumeSyncData() { | ||||
|         try { | ||||
|             // the update process should be synchronous as a whole but individual handlers can run in parallel | ||||
|             await Promise.all([ | ||||
|                 ...allSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, allSyncData)), | ||||
|                 () => appContext.trigger('syncData', {data: allSyncData}), | ||||
|                 ...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData)) | ||||
|             ]); | ||||
|         } | ||||
| @@ -214,7 +210,6 @@ subscribeToMessages(message => { | ||||
| export default { | ||||
|     logError, | ||||
|     subscribeToMessages, | ||||
|     subscribeToAllSyncMessages, | ||||
|     subscribeToOutsideSyncMessages, | ||||
|     waitForSyncId, | ||||
|     waitForMaxKnownSyncId | ||||
|   | ||||
| @@ -533,6 +533,37 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     syncDataListener({data}) { | ||||
|         const noteIdsToRefresh = new Set(); | ||||
|  | ||||
|         // this has the problem that the former parentNoteId might not be invalidated | ||||
|         // and the former location of the branch/note won't be removed. | ||||
|         data.filter(sync => sync.entityName === 'branches').forEach(sync => { | ||||
|             const branch = treeCache.getBranch(sync.entityId); | ||||
|             // we assume that the cache contains the old branch state and we add also the old parentNoteId | ||||
|             // so that the old parent can also be updated | ||||
|             noteIdsToRefresh.add(branch.parentNoteId); | ||||
|  | ||||
|             noteIdsToRefresh.add(sync.parentNoteId); | ||||
|         }); | ||||
|  | ||||
|         data.filter(sync => sync.entityName === 'notes').forEach(sync => noteIdsToRefresh.add(sync.entityId)); | ||||
|  | ||||
|         data.filter(sync => sync.entityName === 'note_reordering').forEach(sync => noteIdsToRefresh.add(sync.entityId)); | ||||
|  | ||||
|         data.filter(sync => sync.entityName === 'attributes').forEach(sync => { | ||||
|             const note = treeCache.notes[sync.noteId]; | ||||
|  | ||||
|             if (note && note.__attributeCache) { | ||||
|                 noteIdsToRefresh.add(sync.entityId); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (noteIdsToRefresh.size > 0) { | ||||
|             appContext.trigger('reloadNotes', {noteIds: Array.from(noteIdsToRefresh)}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     hoistedNoteChangedListener() { | ||||
|         this.reloadTreeListener(); | ||||
|     } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import server from "../../services/server.js"; | ||||
| import noteDetailService from "../../services/note_detail.js"; | ||||
| import linkService from "../../services/link.js"; | ||||
| import libraryLoader from "../../services/library_loader.js"; | ||||
| import treeService from "../../services/tree.js"; | ||||
|   | ||||
| @@ -22,8 +22,6 @@ async function getNote(req) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     await treeService.setCssClassesToNotes([note]); | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| @@ -35,8 +33,6 @@ async function createNote(req) { | ||||
|  | ||||
|     const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params); | ||||
|  | ||||
|     await treeService.setCssClassesToNotes([note]); | ||||
|  | ||||
|     return { | ||||
|         note, | ||||
|         branch | ||||
|   | ||||
| @@ -8,7 +8,14 @@ async function getNotesAndBranches(noteIds) { | ||||
|     noteIds = Array.from(new Set(noteIds)); | ||||
|     const notes = await treeService.getNotes(noteIds); | ||||
|  | ||||
|     noteIds = notes.map(n => n.noteId); | ||||
|     const noteMap = {}; | ||||
|     noteIds = []; | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         note.attributes = []; | ||||
|         noteMap[note.noteId] = note; | ||||
|         noteIds.push(note.noteId); | ||||
|     } | ||||
|  | ||||
|     // joining child note to filter out not completely synchronised notes which would then cause errors later | ||||
|     // cannot do that with parent because of root note's 'none' parent | ||||
| @@ -28,6 +35,27 @@ async function getNotesAndBranches(noteIds) { | ||||
|     // sorting in memory is faster | ||||
|     branches.sort((a, b) => a.notePosition - b.notePosition < 0 ? -1 : 1); | ||||
|  | ||||
|     const attributes = await sql.getManyRows(` | ||||
|         SELECT | ||||
|             noteId, | ||||
|             type, | ||||
|             name, | ||||
|             value, | ||||
|             isInheritable | ||||
|         FROM attributes | ||||
|         WHERE isDeleted = 0 AND noteId IN (???)`, noteIds); | ||||
|  | ||||
|     for (const {noteId, type, name, value, isInheritable} of attributes) { | ||||
|         const note = noteMap[noteId]; | ||||
|  | ||||
|         note.attributes.push({ | ||||
|             type, | ||||
|             name, | ||||
|             value, | ||||
|             isInheritable | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         branches, | ||||
|         notes | ||||
|   | ||||
| @@ -4,59 +4,9 @@ const sql = require('./sql'); | ||||
| const repository = require('./repository'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const syncTableService = require('./sync_table'); | ||||
| const log = require('./log'); | ||||
| const protectedSessionService = require('./protected_session'); | ||||
| const noteCacheService = require('./note_cache'); | ||||
|  | ||||
| async function setCssClassesToNotes(notes) { | ||||
|     const noteIds = notes.map(note => note.noteId); | ||||
|     const noteMap = new Map(notes.map(note => [note.noteId, note])); | ||||
|  | ||||
|     const templateClassLabels = await sql.getManyRows(` | ||||
|         SELECT  | ||||
|           templAttr.noteId,  | ||||
|           attr.name,  | ||||
|           attr.value  | ||||
|         FROM attributes templAttr | ||||
|         JOIN attributes attr ON attr.noteId = templAttr.value | ||||
|         WHERE  | ||||
|           templAttr.isDeleted = 0  | ||||
|           AND templAttr.type = 'relation' | ||||
|           AND templAttr.name = 'template' | ||||
|           AND templAttr.noteId IN (???) | ||||
|           AND attr.isDeleted = 0 | ||||
|           AND attr.type = 'label' | ||||
|           AND attr.name IN ('cssClass', 'iconClass')`, noteIds); | ||||
|  | ||||
|     const noteClassLabels = await sql.getManyRows(` | ||||
|         SELECT  | ||||
|            noteId, name, value  | ||||
|         FROM attributes  | ||||
|         WHERE  | ||||
|            isDeleted = 0  | ||||
|            AND type = 'label'  | ||||
|            AND name IN ('cssClass', 'iconClass')  | ||||
|            AND noteId IN (???)`, noteIds); | ||||
|  | ||||
|     // first template ones, then on the note itself so that note class label have priority | ||||
|     // over template ones for iconClass (which can be only one) | ||||
|     const allClassLabels = templateClassLabels.concat(noteClassLabels); | ||||
|  | ||||
|     for (const label of allClassLabels) { | ||||
|         const note = noteMap.get(label.noteId); | ||||
|  | ||||
|         if (note) { | ||||
|             if (label.name === 'cssClass') { | ||||
|                 note.cssClass = note.cssClass ? `${note.cssClass} ${label.value}` : label.value; | ||||
|             } else if (label.name === 'iconClass') { | ||||
|                 note.iconClass = label.value; | ||||
|             } else { | ||||
|                 log.error(`Unrecognized label name ${label.name}`); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function getNotes(noteIds) { | ||||
|     // we return also deleted notes which have been specifically asked for | ||||
|     const notes = await sql.getManyRows(` | ||||
| @@ -71,15 +21,12 @@ async function getNotes(noteIds) { | ||||
|         FROM notes | ||||
|         WHERE noteId IN (???)`, noteIds); | ||||
|  | ||||
|     await setCssClassesToNotes(notes); | ||||
|  | ||||
|     protectedSessionService.decryptNotes(notes); | ||||
|  | ||||
|     await noteCacheService.loadedPromise; | ||||
|  | ||||
|     notes.forEach(note => { | ||||
|         note.isProtected = !!note.isProtected; | ||||
|         note.archived = noteCacheService.isArchived(note.noteId) | ||||
|         note.isProtected = !!note.isProtected | ||||
|     }); | ||||
|  | ||||
|     return notes; | ||||
| @@ -254,6 +201,5 @@ module.exports = { | ||||
|     validateParentChild, | ||||
|     getBranch, | ||||
|     sortNotesAlphabetically, | ||||
|     setNoteToParent, | ||||
|     setCssClassesToNotes | ||||
|     setNoteToParent | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user