From 60c908cd63b0167a2090ef7442eafbb138e4ee86 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 25 Jan 2020 11:52:45 +0100 Subject: [PATCH] frontend attribute cache refactoring WIP --- src/public/javascripts/entities/attribute.js | 8 +-- src/public/javascripts/entities/note_short.js | 14 ++--- .../javascripts/services/note_detail.js | 4 -- src/public/javascripts/services/tree.js | 27 +-------- src/public/javascripts/services/tree_cache.js | 32 +++++++++- src/public/javascripts/services/ws.js | 9 +-- src/public/javascripts/widgets/note_tree.js | 31 ++++++++++ .../widgets/type_widgets/relation_map.js | 1 - src/routes/api/notes.js | 4 -- src/routes/api/tree.js | 30 +++++++++- src/services/tree.js | 58 +------------------ 11 files changed, 102 insertions(+), 116 deletions(-) diff --git a/src/public/javascripts/entities/attribute.js b/src/public/javascripts/entities/attribute.js index 18d0fdb27..cff714908 100644 --- a/src/public/javascripts/entities/attribute.js +++ b/src/public/javascripts/entities/attribute.js @@ -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})`; } } diff --git a/src/public/javascripts/entities/note_short.js b/src/public/javascripts/entities/note_short.js index be793b25f..6af9c3283 100644 --- a/src/public/javascripts/entities/note_short.js +++ b/src/public/javascripts/entities/note_short.js @@ -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; } diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 0c573e89a..2d4a2bf3b 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -49,10 +49,6 @@ ws.subscribeToOutsideSyncMessages(syncData => { } }); -ws.subscribeToAllSyncMessages(syncData => { - appContext.trigger('syncData', {data: syncData}); -}); - function noteChanged() { const activeTabContext = appContext.getActiveTabContext(); diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index e80d3fcde..75d704096 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -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(); diff --git a/src/public/javascripts/services/tree_cache.js b/src/public/javascripts/services/tree_cache.js index 737e6c349..6b5468c73 100644 --- a/src/public/javascripts/services/tree_cache.js +++ b/src/public/javascripts/services/tree_cache.js @@ -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.} */ this.branches = {}; + + /** @type {Object.} */ + 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) { diff --git a/src/public/javascripts/services/ws.js b/src/public/javascripts/services/ws.js index 754d58988..7b7f4aa41 100644 --- a/src/public/javascripts/services/ws.js +++ b/src/public/javascripts/services/ws.js @@ -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 diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js index d62c0f6b9..473d7ce28 100644 --- a/src/public/javascripts/widgets/note_tree.js +++ b/src/public/javascripts/widgets/note_tree.js @@ -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(); } diff --git a/src/public/javascripts/widgets/type_widgets/relation_map.js b/src/public/javascripts/widgets/type_widgets/relation_map.js index be4e04720..a517c4d97 100644 --- a/src/public/javascripts/widgets/type_widgets/relation_map.js +++ b/src/public/javascripts/widgets/type_widgets/relation_map.js @@ -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"; diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index a57ca8a81..102b8e928 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.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 diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js index eaa478f1b..746834bd2 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.js @@ -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 diff --git a/src/services/tree.js b/src/services/tree.js index 007488f17..4d311e8e3 100644 --- a/src/services/tree.js +++ b/src/services/tree.js @@ -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 }; \ No newline at end of file