mirror of
https://github.com/zadam/trilium.git
synced 2025-01-16 20:21:43 +08:00
frontend attribute cache refactoring WIP
This commit is contained in:
parent
52a907651e
commit
60c908cd63
11 changed files with 102 additions and 116 deletions
|
@ -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…
Reference in a new issue