mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	frontend attribute cache refactoring WIP
This commit is contained in:
		
							parent
							
								
									52a907651e
								
							
						
					
					
						commit
						60c908cd63
					
				@ -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
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user