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; |         this.position = row.position; | ||||||
|         /** @param {boolean} isInheritable */ |         /** @param {boolean} isInheritable */ | ||||||
|         this.isInheritable = row.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} */ |     /** @returns {NoteShort} */ | ||||||
| @@ -29,7 +23,7 @@ class Attribute { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     get toString() { |     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; |         this.mime = row.mime; | ||||||
|         /** @param {boolean} */ |         /** @param {boolean} */ | ||||||
|         this.isDeleted = row.isDeleted; |         this.isDeleted = row.isDeleted; | ||||||
|         /** @param {boolean} */ |  | ||||||
|         this.archived = row.archived; |         /** @type {string[]} */ | ||||||
|         /** @param {string} */ |         this.attributes = []; | ||||||
|         this.cssClass = row.cssClass; |  | ||||||
|         /** @param {string} */ |         /** @type {string[]} */ | ||||||
|         this.iconClass = row.iconClass; |         this.targetRelations = []; | ||||||
|  |  | ||||||
|         /** @type {string[]} */ |         /** @type {string[]} */ | ||||||
|         this.parents = []; |         this.parents = []; | ||||||
| @@ -306,7 +306,7 @@ class NoteShort { | |||||||
|      * Clear note's attributes cache to force fresh reload for next attribute request. |      * Clear note's attributes cache to force fresh reload for next attribute request. | ||||||
|      * Cache is note instance scoped. |      * Cache is note instance scoped. | ||||||
|      */ |      */ | ||||||
|     invalidate__attributeCache() { |     invalidateAttributeCache() { | ||||||
|         this.__attributeCache = null; |         this.__attributeCache = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -49,10 +49,6 @@ ws.subscribeToOutsideSyncMessages(syncData => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| ws.subscribeToAllSyncMessages(syncData => { |  | ||||||
|     appContext.trigger('syncData', {data: syncData}); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| function noteChanged() { | function noteChanged() { | ||||||
|     const activeTabContext = appContext.getActiveTabContext(); |     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 | $(window).on('hashchange', function() { | ||||||
| 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() { |  | ||||||
|     if (isNotePathInAddress()) { |     if (isNotePathInAddress()) { | ||||||
|         const [notePath, tabId] = getHashValueFromAddress(); |         const [notePath, tabId] = getHashValueFromAddress(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import Branch from "../entities/branch.js"; | |||||||
| import NoteShort from "../entities/note_short.js"; | import NoteShort from "../entities/note_short.js"; | ||||||
| import ws from "./ws.js"; | import ws from "./ws.js"; | ||||||
| import server from "./server.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. |  * TreeCache keeps a read only cache of note tree structure in frontend's memory. | ||||||
| @@ -22,15 +23,18 @@ class TreeCache { | |||||||
|  |  | ||||||
|         /** @type {Object.<string, Branch>} */ |         /** @type {Object.<string, Branch>} */ | ||||||
|         this.branches = {}; |         this.branches = {}; | ||||||
|  |  | ||||||
|  |         /** @type {Object.<string, Attribute>} */ | ||||||
|  |         this.attributes = {}; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     load(noteRows, branchRows) { |     load(noteRows, branchRows, attributeRows) { | ||||||
|         this.init(); |         this.init(); | ||||||
|  |  | ||||||
|         this.addResp(noteRows, branchRows); |         this.addResp(noteRows, branchRows, attributeRows); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     addResp(noteRows, branchRows) { |     addResp(noteRows, branchRows, attributeRows) { | ||||||
|         const branchesByNotes = {}; |         const branchesByNotes = {}; | ||||||
|  |  | ||||||
|         for (const branchRow of branchRows) { |         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) { |     async reloadNotes(noteIds) { | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import utils from './utils.js'; | import utils from './utils.js'; | ||||||
| import toastService from "./toast.js"; | import toastService from "./toast.js"; | ||||||
| import server from "./server.js"; | import server from "./server.js"; | ||||||
|  | import appContext from "./app_context.js"; | ||||||
|  |  | ||||||
| const $outstandingSyncsCount = $("#outstanding-syncs-count"); | const $outstandingSyncsCount = $("#outstanding-syncs-count"); | ||||||
|  |  | ||||||
| const allSyncMessageHandlers = []; |  | ||||||
| const outsideSyncMessageHandlers = []; | const outsideSyncMessageHandlers = []; | ||||||
| const messageHandlers = []; | const messageHandlers = []; | ||||||
|  |  | ||||||
| @@ -34,10 +34,6 @@ function subscribeToOutsideSyncMessages(messageHandler) { | |||||||
|     outsideSyncMessageHandlers.push(messageHandler); |     outsideSyncMessageHandlers.push(messageHandler); | ||||||
| } | } | ||||||
|  |  | ||||||
| function subscribeToAllSyncMessages(messageHandler) { |  | ||||||
|     allSyncMessageHandlers.push(messageHandler); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // used to serialize sync operations | // used to serialize sync operations | ||||||
| let consumeQueuePromise = null; | let consumeQueuePromise = null; | ||||||
|  |  | ||||||
| @@ -139,7 +135,7 @@ async function consumeSyncData() { | |||||||
|         try { |         try { | ||||||
|             // the update process should be synchronous as a whole but individual handlers can run in parallel |             // the update process should be synchronous as a whole but individual handlers can run in parallel | ||||||
|             await Promise.all([ |             await Promise.all([ | ||||||
|                 ...allSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, allSyncData)), |                 () => appContext.trigger('syncData', {data: allSyncData}), | ||||||
|                 ...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData)) |                 ...outsideSyncMessageHandlers.map(syncHandler => runSafely(syncHandler, outsideSyncData)) | ||||||
|             ]); |             ]); | ||||||
|         } |         } | ||||||
| @@ -214,7 +210,6 @@ subscribeToMessages(message => { | |||||||
| export default { | export default { | ||||||
|     logError, |     logError, | ||||||
|     subscribeToMessages, |     subscribeToMessages, | ||||||
|     subscribeToAllSyncMessages, |  | ||||||
|     subscribeToOutsideSyncMessages, |     subscribeToOutsideSyncMessages, | ||||||
|     waitForSyncId, |     waitForSyncId, | ||||||
|     waitForMaxKnownSyncId |     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() { |     hoistedNoteChangedListener() { | ||||||
|         this.reloadTreeListener(); |         this.reloadTreeListener(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import noteDetailService from "../../services/note_detail.js"; |  | ||||||
| import linkService from "../../services/link.js"; | import linkService from "../../services/link.js"; | ||||||
| import libraryLoader from "../../services/library_loader.js"; | import libraryLoader from "../../services/library_loader.js"; | ||||||
| import treeService from "../../services/tree.js"; | import treeService from "../../services/tree.js"; | ||||||
|   | |||||||
| @@ -22,8 +22,6 @@ async function getNote(req) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     await treeService.setCssClassesToNotes([note]); |  | ||||||
|  |  | ||||||
|     return note; |     return note; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -35,8 +33,6 @@ async function createNote(req) { | |||||||
|  |  | ||||||
|     const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params); |     const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params); | ||||||
|  |  | ||||||
|     await treeService.setCssClassesToNotes([note]); |  | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         note, |         note, | ||||||
|         branch |         branch | ||||||
|   | |||||||
| @@ -8,7 +8,14 @@ async function getNotesAndBranches(noteIds) { | |||||||
|     noteIds = Array.from(new Set(noteIds)); |     noteIds = Array.from(new Set(noteIds)); | ||||||
|     const notes = await treeService.getNotes(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 |     // 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 |     // 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 |     // sorting in memory is faster | ||||||
|     branches.sort((a, b) => a.notePosition - b.notePosition < 0 ? -1 : 1); |     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 { |     return { | ||||||
|         branches, |         branches, | ||||||
|         notes |         notes | ||||||
|   | |||||||
| @@ -4,59 +4,9 @@ const sql = require('./sql'); | |||||||
| const repository = require('./repository'); | const repository = require('./repository'); | ||||||
| const Branch = require('../entities/branch'); | const Branch = require('../entities/branch'); | ||||||
| const syncTableService = require('./sync_table'); | const syncTableService = require('./sync_table'); | ||||||
| const log = require('./log'); |  | ||||||
| const protectedSessionService = require('./protected_session'); | const protectedSessionService = require('./protected_session'); | ||||||
| const noteCacheService = require('./note_cache'); | 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) { | async function getNotes(noteIds) { | ||||||
|     // we return also deleted notes which have been specifically asked for |     // we return also deleted notes which have been specifically asked for | ||||||
|     const notes = await sql.getManyRows(` |     const notes = await sql.getManyRows(` | ||||||
| @@ -71,15 +21,12 @@ async function getNotes(noteIds) { | |||||||
|         FROM notes |         FROM notes | ||||||
|         WHERE noteId IN (???)`, noteIds); |         WHERE noteId IN (???)`, noteIds); | ||||||
|  |  | ||||||
|     await setCssClassesToNotes(notes); |  | ||||||
|  |  | ||||||
|     protectedSessionService.decryptNotes(notes); |     protectedSessionService.decryptNotes(notes); | ||||||
|  |  | ||||||
|     await noteCacheService.loadedPromise; |     await noteCacheService.loadedPromise; | ||||||
|  |  | ||||||
|     notes.forEach(note => { |     notes.forEach(note => { | ||||||
|         note.isProtected = !!note.isProtected; |         note.isProtected = !!note.isProtected | ||||||
|         note.archived = noteCacheService.isArchived(note.noteId) |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return notes; |     return notes; | ||||||
| @@ -254,6 +201,5 @@ module.exports = { | |||||||
|     validateParentChild, |     validateParentChild, | ||||||
|     getBranch, |     getBranch, | ||||||
|     sortNotesAlphabetically, |     sortNotesAlphabetically, | ||||||
|     setNoteToParent, |     setNoteToParent | ||||||
|     setCssClassesToNotes |  | ||||||
| }; | }; | ||||||
		Reference in New Issue
	
	Block a user