frontend attribute cache refactoring WIP

This commit is contained in:
zadam 2020-01-25 11:52:45 +01:00
parent 52a907651e
commit 60c908cd63
11 changed files with 102 additions and 116 deletions

View file

@ -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})`;
}
}

View file

@ -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;
}

View file

@ -49,10 +49,6 @@ ws.subscribeToOutsideSyncMessages(syncData => {
}
});
ws.subscribeToAllSyncMessages(syncData => {
appContext.trigger('syncData', {data: syncData});
});
function noteChanged() {
const activeTabContext = appContext.getActiveTabContext();

View file

@ -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();

View file

@ -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) {

View file

@ -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

View file

@ -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();
}

View file

@ -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";

View file

@ -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

View file

@ -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

View file

@ -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
};