mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
note cache refactoring + handling entity changes
This commit is contained in:
parent
ccb5f3ee18
commit
b07accfd9d
@ -27,6 +27,8 @@ class Note {
|
|||||||
this.title = row.title;
|
this.title = row.title;
|
||||||
/** @param {boolean} */
|
/** @param {boolean} */
|
||||||
this.isProtected = !!row.isProtected;
|
this.isProtected = !!row.isProtected;
|
||||||
|
/** @param {boolean} */
|
||||||
|
this.isDecrypted = false;
|
||||||
/** @param {Note[]} */
|
/** @param {Note[]} */
|
||||||
this.parents = [];
|
this.parents = [];
|
||||||
/** @param {Note[]} */
|
/** @param {Note[]} */
|
||||||
@ -82,6 +84,16 @@ class Note {
|
|||||||
get isArchived() {
|
get isArchived() {
|
||||||
return this.hasAttribute('label', 'archived');
|
return this.hasAttribute('label', 'archived');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasInheritableOwnedArchivedLabel() {
|
||||||
|
return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// will sort the parents so that non-archived are first and archived at the end
|
||||||
|
// this is done so that non-archived paths are always explored as first when searching for note path
|
||||||
|
resortParents() {
|
||||||
|
this.parents.sort((a, b) => a.hasInheritableOwnedArchivedLabel ? 1 : -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Branch {
|
class Branch {
|
||||||
@ -94,6 +106,8 @@ class Branch {
|
|||||||
this.parentNoteId = row.parentNoteId;
|
this.parentNoteId = row.parentNoteId;
|
||||||
/** @param {string} */
|
/** @param {string} */
|
||||||
this.prefix = row.prefix;
|
this.prefix = row.prefix;
|
||||||
|
|
||||||
|
childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return {Note} */
|
/** @return {Note} */
|
||||||
@ -172,12 +186,6 @@ let loadedPromiseResolve;
|
|||||||
/** Is resolved after the initial load */
|
/** Is resolved after the initial load */
|
||||||
let loadedPromise = new Promise(res => loadedPromiseResolve = res);
|
let loadedPromise = new Promise(res => loadedPromiseResolve = res);
|
||||||
|
|
||||||
let noteTitles = {};
|
|
||||||
let protectedNoteTitles = {};
|
|
||||||
let noteIds;
|
|
||||||
const childToParent = {};
|
|
||||||
let archived = {};
|
|
||||||
|
|
||||||
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
||||||
let prefixes = {};
|
let prefixes = {};
|
||||||
|
|
||||||
@ -236,8 +244,6 @@ async function load() {
|
|||||||
|
|
||||||
childNote.parents.push(parentNote);
|
childNote.parents.push(parentNote);
|
||||||
parentNote.children.push(childNote);
|
parentNote.children.push(childNote);
|
||||||
|
|
||||||
childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`] = branch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||||
@ -254,12 +260,31 @@ async function load() {
|
|||||||
|
|
||||||
async function decryptProtectedNotes() {
|
async function decryptProtectedNotes() {
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.isProtected) {
|
if (note.isProtected && !note.isDecrypted) {
|
||||||
note.title = protectedSessionService.decryptString(note.title);
|
note.title = protectedSessionService.decryptString(note.title);
|
||||||
|
|
||||||
|
note.isDecrypted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAttribute(attr) {
|
||||||
|
if (attr.type === 'relation') {
|
||||||
|
return '@' + utils.escapeHtml(attr.name) + "=…";
|
||||||
|
}
|
||||||
|
else if (attr.type === 'label') {
|
||||||
|
let label = '#' + utils.escapeHtml(attr.name);
|
||||||
|
|
||||||
|
if (attr.value) {
|
||||||
|
const val = /[^\w_-]/.test(attr.value) ? '"' + attr.value + '"' : attr.value;
|
||||||
|
|
||||||
|
label += '=' + utils.escapeHtml(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function highlightResults(results, allTokens) {
|
function highlightResults(results, allTokens) {
|
||||||
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
|
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
|
||||||
// which would make the resulting HTML string invalid.
|
// which would make the resulting HTML string invalid.
|
||||||
@ -274,7 +299,7 @@ function highlightResults(results, allTokens) {
|
|||||||
|
|
||||||
for (const attr of note.attributes) {
|
for (const attr of note.attributes) {
|
||||||
if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
|
if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
|
||||||
result.pathTitle += ` <small>@${attr.name}=${attr.value}</small>`;
|
result.pathTitle += ` <small>${formatAttribute(attr)}</small>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,6 +321,44 @@ function highlightResults(results, allTokens) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns noteIds which have at least one matching tokens
|
||||||
|
*
|
||||||
|
* @param tokens
|
||||||
|
* @return {Set<String>}
|
||||||
|
*/
|
||||||
|
function getCandidateNotes(tokens) {
|
||||||
|
const candidateNoteIds = new Set();
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
for (const noteId in fulltext) {
|
||||||
|
if (!fulltext[noteId].includes(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateNoteIds.add(noteId);
|
||||||
|
const note = notes[noteId];
|
||||||
|
const inheritableAttrs = note.ownedAttributes.filter(attr => attr.isInheritable);
|
||||||
|
|
||||||
|
searchingAttrs:
|
||||||
|
// for matching inheritable attributes, include the whole note subtree to the candidates
|
||||||
|
for (const attr of inheritableAttrs) {
|
||||||
|
const lcName = attr.name.toLowerCase();
|
||||||
|
const lcValue = attr.value.toLowerCase();
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (lcName.includes(token) || lcValue.includes(token)) {
|
||||||
|
note.addSubTreeNoteIdsTo(candidateNoteIds);
|
||||||
|
|
||||||
|
break searchingAttrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidateNoteIds;
|
||||||
|
}
|
||||||
|
|
||||||
async function findNotes(query) {
|
async function findNotes(query) {
|
||||||
if (!query.length) {
|
if (!query.length) {
|
||||||
return [];
|
return [];
|
||||||
@ -307,41 +370,14 @@ async function findNotes(query) {
|
|||||||
.split(/[ -]/)
|
.split(/[ -]/)
|
||||||
.filter(token => token !== '/'); // '/' is used as separator
|
.filter(token => token !== '/'); // '/' is used as separator
|
||||||
|
|
||||||
const tokens = allTokens.slice();
|
const candidateNoteIds = getCandidateNotes(allTokens);
|
||||||
|
|
||||||
const matchedNoteIds = new Set();
|
|
||||||
|
|
||||||
for (const token of tokens) {
|
|
||||||
for (const noteId in fulltext) {
|
|
||||||
if (!fulltext[noteId].includes(token)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
matchedNoteIds.add(noteId);
|
|
||||||
const note = notes[noteId];
|
|
||||||
const inheritableAttrs = note.ownedAttributes.filter(attr => attr.isInheritable);
|
|
||||||
|
|
||||||
searchingAttrs:
|
|
||||||
for (const attr of inheritableAttrs) {
|
|
||||||
const lcName = attr.name.toLowerCase();
|
|
||||||
const lcValue = attr.value.toLowerCase();
|
|
||||||
|
|
||||||
for (const token of tokens) {
|
|
||||||
if (lcName.includes(token) || lcValue.includes(token)) {
|
|
||||||
note.addSubTreeNoteIdsTo(matchedNoteIds);
|
|
||||||
|
|
||||||
break searchingAttrs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//console.log(matchedNoteIds);
|
|
||||||
// now we have set of noteIds which match at least one token
|
// now we have set of noteIds which match at least one token
|
||||||
|
|
||||||
let results = [];
|
let results = [];
|
||||||
|
const tokens = allTokens.slice();
|
||||||
|
|
||||||
for (const noteId of matchedNoteIds) {
|
for (const noteId of candidateNoteIds) {
|
||||||
const note = notes[noteId];
|
const note = notes[noteId];
|
||||||
|
|
||||||
// autocomplete should be able to find notes by their noteIds as well (only leafs)
|
// autocomplete should be able to find notes by their noteIds as well (only leafs)
|
||||||
@ -476,14 +512,17 @@ function search(note, tokens, path, results) {
|
|||||||
|
|
||||||
function isNotePathArchived(notePath) {
|
function isNotePathArchived(notePath) {
|
||||||
const noteId = notePath[notePath.length - 1];
|
const noteId = notePath[notePath.length - 1];
|
||||||
|
const note = notes[noteId];
|
||||||
|
|
||||||
if (archived[noteId] !== undefined) {
|
if (note.isArchived) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < notePath.length - 1; i++) {
|
for (let i = 0; i < notePath.length - 1; i++) {
|
||||||
|
const note = notes[notePath[i]];
|
||||||
|
|
||||||
// this is going through parents so archived must be inheritable
|
// this is going through parents so archived must be inheritable
|
||||||
if (archived[notePath[i]] === 1) {
|
if (note.hasInheritableOwnedArchivedLabel) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -550,7 +589,7 @@ function getNoteTitle(childNoteId, parentNoteId) {
|
|||||||
|
|
||||||
const branch = parentNote ? getBranch(childNote.noteId, parentNote.noteId) : null;
|
const branch = parentNote ? getBranch(childNote.noteId, parentNote.noteId) : null;
|
||||||
|
|
||||||
return ((branch && branch.prefix) ? (branch.prefix + ' - ') : '') + title;
|
return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteTitleArrayForPath(path) {
|
function getNoteTitleArrayForPath(path) {
|
||||||
@ -639,11 +678,11 @@ function getNotePath(noteId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateSimilarity(text1, text2, noteId, results) {
|
function evaluateSimilarity(text, note, results) {
|
||||||
let coeff = stringSimilarity.compareTwoStrings(text1, text2);
|
let coeff = stringSimilarity.compareTwoStrings(text, note.title);
|
||||||
|
|
||||||
if (coeff > 0.4) {
|
if (coeff > 0.4) {
|
||||||
const notePath = getSomePath(noteId);
|
const notePath = getSomePath(note);
|
||||||
|
|
||||||
// this takes care of note hoisting
|
// this takes care of note hoisting
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
@ -654,7 +693,7 @@ function evaluateSimilarity(text1, text2, noteId, results) {
|
|||||||
coeff -= 0.2; // archived penalization
|
coeff -= 0.2; // archived penalization
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({coeff, notePath, noteId});
|
results.push({coeff, notePath, noteId: note.noteId});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,11 +707,16 @@ function setImmediatePromise() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluateSimilarityDict(title, dict, results) {
|
async function findSimilarNotes(title) {
|
||||||
|
const results = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
for (const noteId in dict) {
|
for (const note of Object.values(notes)) {
|
||||||
evaluateSimilarity(title, dict[noteId], noteId, results);
|
if (note.isProtected && !note.isDecrypted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateSimilarity(title, note, results);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
@ -680,16 +724,6 @@ async function evaluateSimilarityDict(title, dict, results) {
|
|||||||
await setImmediatePromise();
|
await setImmediatePromise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function findSimilarNotes(title) {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await evaluateSimilarityDict(title, noteTitles, results);
|
|
||||||
|
|
||||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
|
||||||
await evaluateSimilarityDict(title, protectedNoteTitles, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort((a, b) => a.coeff > b.coeff ? -1 : 1);
|
results.sort((a, b) => a.coeff > b.coeff ? -1 : 1);
|
||||||
|
|
||||||
@ -704,80 +738,84 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entityName === 'notes') {
|
if (entityName === 'notes') {
|
||||||
const note = entity;
|
const {noteId} = entity;
|
||||||
|
|
||||||
if (note.isDeleted) {
|
if (entity.isDeleted) {
|
||||||
delete noteTitles[note.noteId];
|
delete notes[noteId];
|
||||||
delete childToParent[note.noteId];
|
}
|
||||||
|
else if (noteId in notes) {
|
||||||
|
// we can assume we have protected session since we managed to update
|
||||||
|
notes[noteId].title = entity.title;
|
||||||
|
notes[noteId].isDecrypted = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (note.isProtected) {
|
notes[noteId] = new Note(entity);
|
||||||
// we can assume we have protected session since we managed to update
|
|
||||||
// removing from the maps is important when switching between protected & unprotected
|
|
||||||
protectedNoteTitles[note.noteId] = note.title;
|
|
||||||
delete noteTitles[note.noteId];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
noteTitles[note.noteId] = note.title;
|
|
||||||
delete protectedNoteTitles[note.noteId];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (entityName === 'branches') {
|
else if (entityName === 'branches') {
|
||||||
const branch = entity;
|
const {branchId, noteId, parentNoteId} = entity;
|
||||||
|
|
||||||
if (branch.isDeleted) {
|
if (entity.isDeleted) {
|
||||||
if (branch.noteId in childToParent) {
|
const childNote = notes[noteId];
|
||||||
childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId);
|
|
||||||
|
if (childNote) {
|
||||||
|
childNote.parents = childNote.parents.filter(parent => parent.noteId !== parentNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
|
const parentNote = notes[parentNoteId];
|
||||||
delete childParentToBranchId[branch.noteId + '-' + branch.parentNoteId];
|
|
||||||
|
if (parentNote) {
|
||||||
|
childNote.children = childNote.children.filter(child => child.noteId !== noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete childParentToBranch[`${noteId}-${parentNoteId}`];
|
||||||
|
delete branches[branchId];
|
||||||
|
}
|
||||||
|
else if (branchId in branches) {
|
||||||
|
// only relevant thing which can change in a branch is prefix
|
||||||
|
branches[branchId].prefix = entity.prefix;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (branch.prefix) {
|
branches[branchId] = new Branch(entity);
|
||||||
prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
|
|
||||||
|
const note = notes[entity.noteId];
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
note.resortParents();
|
||||||
}
|
}
|
||||||
|
|
||||||
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
|
|
||||||
|
|
||||||
if (!childToParent[branch.noteId].includes(branch.parentNoteId)) {
|
|
||||||
childToParent[branch.noteId].push(branch.parentNoteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
resortChildToParent(branch.noteId);
|
|
||||||
|
|
||||||
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (entityName === 'attributes') {
|
else if (entityName === 'attributes') {
|
||||||
const attribute = entity;
|
const {attributeId, noteId} = entity;
|
||||||
|
|
||||||
if (attribute.type === 'label' && attribute.name === 'archived') {
|
if (entity.isDeleted) {
|
||||||
// we're not using label object directly, since there might be other non-deleted archived label
|
const note = notes[noteId];
|
||||||
const archivedLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'
|
|
||||||
AND name = 'archived' AND noteId = ?`, [attribute.noteId]);
|
|
||||||
|
|
||||||
if (archivedLabel) {
|
if (note) {
|
||||||
archived[attribute.noteId] = archivedLabel.isInheritable ? 1 : 0;
|
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attributeId);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
delete archived[attribute.noteId];
|
delete attributes[entity.attributeId];
|
||||||
|
}
|
||||||
|
else if (attributeId in attributes) {
|
||||||
|
const attr = attributes[attributeId];
|
||||||
|
|
||||||
|
// attr name cannot change
|
||||||
|
attr.value = entity.value;
|
||||||
|
attr.isInheritable = entity.isInheritable;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
attributes[attributeId] = new Attribute(entity);
|
||||||
|
|
||||||
|
const note = notes[noteId];
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
note.ownedAttributes.push(attributes[attributeId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// will sort the childs so that non-archived are first and archived at the end
|
|
||||||
// this is done so that non-archived paths are always explored as first when searching for note path
|
|
||||||
function resortChildToParent(noteId) {
|
|
||||||
if (!(noteId in childToParent)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
childToParent[noteId].sort((a, b) => archived[a] === 1 ? 1 : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param noteId
|
* @param noteId
|
||||||
* @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting
|
* @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting
|
||||||
|
Loading…
x
Reference in New Issue
Block a user