mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +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;
|
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
|
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user