trilium/src/public/javascripts/services/tree_cache.js

360 lines
12 KiB
JavaScript
Raw Normal View History

import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js";
import Attribute from "../entities/attribute.js";
2020-01-25 20:46:55 +08:00
import server from "./server.js";
2020-01-28 05:58:03 +08:00
import {LoadResults} from "./load_results.js";
import NoteComplement from "../entities/note_complement.js";
/**
* TreeCache keeps a read only cache of note tree structure in frontend's memory.
2019-10-28 02:17:32 +08:00
* - notes are loaded lazily when unknown noteId is requested
* - when note is loaded, all its parent and child branches are loaded as well. For a branch to be used, it's not must be loaded before
* - deleted notes are present in the cache as well, but they don't have any branches. As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded, we can infer it is deleted.
*
* Note and branch deletions are corner cases and usually not needed.
*/
class TreeCache {
constructor() {
this.init();
}
init() {
/** @type {Object.<string, NoteShort>} */
this.notes = {};
/** @type {Object.<string, Branch>} */
this.branches = {};
/** @type {Object.<string, Attribute>} */
this.attributes = {};
/** @type {Object.<string, Promise<NoteComplement>>} */
this.noteComplementPromises = {};
}
load(noteRows, branchRows, attributeRows) {
this.init();
this.addResp(noteRows, branchRows, attributeRows);
}
addResp(noteRows, branchRows, attributeRows) {
2019-10-26 15:51:08 +08:00
for (const noteRow of noteRows) {
const {noteId} = noteRow;
const oldNote = this.notes[noteId];
if (oldNote) {
for (const childNoteId of oldNote.children) {
const childNote = this.notes[childNoteId];
if (childNote) {
childNote.parents = childNote.parents.filter(p => p !== noteId);
2019-10-28 02:17:32 +08:00
delete this.branches[childNote.parentToBranch[noteId]];
2019-10-26 15:51:08 +08:00
delete childNote.parentToBranch[noteId];
}
}
2019-04-14 04:10:16 +08:00
2019-10-26 15:51:08 +08:00
for (const parentNoteId of oldNote.parents) {
const parentNote = this.notes[parentNoteId];
2019-04-14 04:10:16 +08:00
2019-10-26 15:51:08 +08:00
if (parentNote) {
parentNote.children = parentNote.children.filter(p => p !== noteId);
2019-04-14 04:10:16 +08:00
2019-10-28 02:17:32 +08:00
delete this.branches[parentNote.childToBranch[noteId]];
2019-10-26 15:51:08 +08:00
delete parentNote.childToBranch[noteId];
}
}
2019-10-20 18:29:34 +08:00
}
2019-04-14 04:10:16 +08:00
2020-02-01 03:52:31 +08:00
const note = new NoteShort(this, noteRow);
2019-04-14 04:10:16 +08:00
2019-10-26 15:51:08 +08:00
this.notes[note.noteId] = note;
2020-02-01 03:52:31 +08:00
}
2019-04-14 04:10:16 +08:00
2020-02-01 03:52:31 +08:00
for (const branchRow of branchRows) {
const branch = new Branch(this, branchRow);
2019-04-14 04:10:16 +08:00
2020-02-01 03:52:31 +08:00
this.branches[branch.branchId] = branch;
const childNote = this.notes[branch.noteId];
if (childNote) {
childNote.addParent(branch.parentNoteId, branch.branchId);
2019-10-26 15:51:08 +08:00
}
2020-02-01 03:52:31 +08:00
const parentNote = this.notes[branch.parentNoteId];
2020-02-01 03:52:31 +08:00
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId);
2019-10-26 15:51:08 +08:00
}
}
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);
}
}
}
}
2019-10-26 15:51:08 +08:00
}
2020-01-26 18:41:40 +08:00
async reloadNotes(noteIds) {
if (noteIds.length === 0) {
return;
}
2020-01-30 03:14:02 +08:00
noteIds = Array.from(new Set(noteIds)); // make noteIds unique
2019-10-26 15:51:08 +08:00
const resp = await server.post('tree/load', { noteIds });
2020-01-25 20:46:55 +08:00
this.addResp(resp.notes, resp.branches, resp.attributes);
for (const note of resp.notes) {
if (note.type === 'search') {
const searchResults = await server.get('search-note/' + note.noteId);
2019-11-17 02:07:32 +08:00
if (!searchResults) {
throw new Error(`Search note ${note.noteId} failed.`);
}
// force to load all the notes at once instead of one by one
2020-01-26 18:41:40 +08:00
await this.getNotes(searchResults.map(res => res.noteId));
const branches = resp.branches.filter(b => b.noteId === note.noteId || b.parentNoteId === note.noteId);
searchResults.forEach((result, index) => branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
branchId: "virt" + result.noteId + '-' + note.noteId,
noteId: result.noteId,
parentNoteId: note.noteId,
2020-01-26 18:41:40 +08:00
prefix: this.getBranch(result.branchId).prefix,
notePosition: (index + 1) * 10
}));
// update this note with standard (parent) branches + virtual (children) branches
2020-01-26 18:41:40 +08:00
this.addResp([note], branches, []);
}
}
}
2019-04-14 04:10:16 +08:00
/** @return {Promise<NoteShort[]>} */
async getNotes(noteIds, silentNotFoundError = false) {
2019-12-17 05:00:44 +08:00
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
2020-01-26 18:41:40 +08:00
await this.reloadNotes(missingNoteIds);
return noteIds.map(noteId => {
if (!this.notes[noteId] && !silentNotFoundError) {
2020-01-25 20:46:55 +08:00
console.log(`Can't find note "${noteId}"`);
2018-08-06 17:30:37 +08:00
2018-08-12 18:59:38 +08:00
return null;
}
else {
return this.notes[noteId];
}
}).filter(note => !!note);
}
2019-04-14 04:10:16 +08:00
/** @return {Promise<boolean>} */
async noteExists(noteId) {
const notes = await this.getNotes([noteId], true);
return notes.length === 1;
}
/** @return {Promise<NoteShort>} */
2019-09-08 17:25:57 +08:00
async getNote(noteId, silentNotFoundError = false) {
2018-05-27 04:16:34 +08:00
if (noteId === 'none') {
console.log(`No 'none' note.`);
return null;
}
else if (!noteId) {
console.log(`Falsy noteId ${noteId}, returning null.`);
2018-05-27 04:16:34 +08:00
return null;
}
2019-09-08 17:25:57 +08:00
return (await this.getNotes([noteId], silentNotFoundError))[0];
}
2020-01-16 04:36:01 +08:00
getNoteFromCache(noteId) {
return this.notes[noteId];
}
getBranches(branchIds) {
return branchIds
.map(branchId => this.getBranch(branchId))
.filter(b => b !== null);
}
/** @return {Branch} */
getBranch(branchId, silentNotFoundError = false) {
if (!(branchId in this.branches)) {
if (!silentNotFoundError) {
console.error(`Not existing branch ${branchId}`);
}
}
2019-10-27 02:48:56 +08:00
else {
return this.branches[branchId];
}
}
2020-01-22 04:43:23 +08:00
async getBranchId(parentNoteId, childNoteId) {
const child = await this.getNote(childNoteId);
return child.parentToBranch[parentNoteId];
}
2020-01-26 17:42:24 +08:00
async getNoteComplement(noteId) {
if (!this.noteComplementPromises[noteId]) {
this.noteComplementPromises[noteId] = server.get('notes/' + noteId).then(row => new NoteComplement(row));
}
return await this.noteComplementPromises[noteId];
}
2020-01-30 04:38:58 +08:00
// FIXME does not actually belong here
2020-01-26 18:41:40 +08:00
async processSyncRows(syncRows) {
2020-01-30 04:38:58 +08:00
const loadResults = new LoadResults(this);
2020-01-26 17:42:24 +08:00
2020-01-30 05:32:22 +08:00
syncRows.filter(sync => sync.entityName === 'notes').forEach(sync => {
2020-01-31 05:38:31 +08:00
const note = this.notes[sync.entityId];
2020-01-30 05:32:22 +08:00
2020-01-31 05:38:31 +08:00
if (note) {
note.update(sync.entity);
loadResults.addNote(sync.entityId, sync.sourceId);
}
2020-01-30 05:32:22 +08:00
});
2020-01-26 18:41:40 +08:00
syncRows.filter(sync => sync.entityName === 'branches').forEach(sync => {
2020-02-01 03:52:31 +08:00
let branch = this.branches[sync.entityId];
const childNote = this.notes[sync.entity.noteId];
const parentNote = this.notes[sync.entity.parentNoteId];
2020-01-30 03:14:02 +08:00
2020-01-31 05:38:31 +08:00
if (branch) {
2020-02-01 03:52:31 +08:00
if (sync.entity.isDeleted) {
if (childNote) {
childNote.parents = childNote.parents.filter(parentNoteId => parentNoteId !== sync.entity.parentNoteId);
delete childNote.parentToBranch[sync.entity.parentNoteId];
}
if (parentNote) {
parentNote.children = parentNote.children.filter(childNoteId => childNoteId !== sync.entity.noteId);
delete parentNote.childToBranch[sync.entity.noteId];
}
}
else {
branch.update(sync.entity);
loadResults.addBranch(sync.entityId, sync.sourceId);
if (childNote) {
childNote.addParent(branch.parentNoteId, branch.branchId);
}
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId);
}
}
}
else if (!sync.entity.isDeleted) {
if (childNote || parentNote) {
branch = new Branch(this, sync.entity);
this.branches[branch.branchId] = branch;
loadResults.addBranch(sync.entityId, sync.sourceId);
if (childNote) {
childNote.addParent(branch.parentNoteId, branch.branchId);
}
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId);
}
}
2020-01-31 05:38:31 +08:00
}
2020-01-30 03:14:02 +08:00
});
2020-01-26 17:42:24 +08:00
2020-01-30 03:14:02 +08:00
syncRows.filter(sync => sync.entityName === 'note_reordering').forEach(sync => {
2020-01-31 05:38:31 +08:00
for (const branchId in sync.positions) {
const branch = this.branches[branchId];
if (branch) {
branch.notePosition = sync.positions[branchId];
}
}
2020-01-26 17:42:24 +08:00
2020-01-30 03:14:02 +08:00
loadResults.addNoteReordering(sync.entityId, sync.sourceId);
});
2020-01-26 17:42:24 +08:00
2020-01-26 18:41:40 +08:00
// missing reloading the relation target note
2020-01-30 03:14:02 +08:00
syncRows.filter(sync => sync.entityName === 'attributes').forEach(sync => {
2020-02-01 03:52:31 +08:00
let attribute = this.attributes[sync.entityId];
const sourceNote = this.notes[sync.entity.noteId];
const targetNote = sync.entity.type === 'relation' && this.notes[sync.entity.value];
2020-01-30 03:14:02 +08:00
2020-01-31 05:38:31 +08:00
if (attribute) {
attribute.update(sync.entity);
loadResults.addAttribute(sync.entityId, sync.sourceId);
2020-02-01 03:52:31 +08:00
if (sync.entity.isDeleted) {
if (sourceNote) {
sourceNote.attributes = sourceNote.attributes.filter(attributeId => attributeId !== attribute.attributeId);
}
if (targetNote) {
targetNote.targetRelations = targetNote.targetRelations.filter(attributeId => attributeId !== attribute.value);
}
}
}
else if (!sync.entity.isDeleted) {
if (sourceNote || targetNote) {
attribute = new Attribute(this, sync.entity);
this.attributes[attribute.attributeId] = attribute;
loadResults.addAttribute(sync.entityId, sync.sourceId);
if (sourceNote && !sourceNote.attributes.includes(attribute.attributeId)) {
sourceNote.attributes.push(attribute.attributeId);
}
if (targetNote && !targetNote.attributes.includes(attribute.attributeId)) {
targetNote.attributes.push(attribute.attributeId);
}
}
2020-01-31 05:38:31 +08:00
}
2020-01-30 03:14:02 +08:00
});
2020-01-26 17:42:24 +08:00
2020-02-01 05:32:24 +08:00
syncRows.filter(sync => sync.entityName === 'note_contents').forEach(sync => {
delete this.noteComplementPromises[sync.entityId];
2020-02-01 05:32:24 +08:00
loadResults.addNoteContent(sync.entityId, sync.sourceId);
});
2020-01-30 04:38:58 +08:00
syncRows.filter(sync => sync.entityName === 'note_revisions').forEach(sync => {
loadResults.addNoteRevision(sync.entityId, sync.noteId, sync.sourceId);
});
2020-01-28 05:58:03 +08:00
const appContext = (await import('./app_context.js')).default;
2020-01-30 03:14:02 +08:00
appContext.trigger('entitiesReloaded', {loadResults});
2020-01-26 17:42:24 +08:00
}
}
const treeCache = new TreeCache();
export default treeCache;