395 lines
13 KiB
JavaScript
Raw Normal View History

import FBranch from "../entities/fbranch.js";
import FNote from "../entities/fnote.js";
import FAttribute from "../entities/fattribute.js";
2020-01-25 13:46:55 +01:00
import server from "./server.js";
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js";
2023-05-05 16:37:39 +02:00
import FBlob from "../entities/fblob.js";
2023-04-17 23:21:28 +02:00
import FAttachment from "../entities/fattachment.js";
/**
2021-10-16 22:13:34 +02:00
* Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory.
2019-10-27 19:17:32 +01: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.
2021-10-16 22:13:34 +02:00
*
* Backend has a similar cache called Becca
*/
2021-04-16 22:57:37 +02:00
class Froca {
constructor() {
this.initializedPromise = this.loadInitialTree();
}
async loadInitialTree() {
const resp = await server.get('tree');
2023-05-05 23:41:11 +02:00
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
/** @type {Object.<string, FNote>} */
this.notes = {};
/** @type {Object.<string, FBranch>} */
this.branches = {};
/** @type {Object.<string, FAttribute>} */
this.attributes = {};
2023-04-03 23:47:24 +02:00
/** @type {Object.<string, FAttachment>} */
this.attachments = {};
2023-05-05 16:37:39 +02:00
/** @type {Object.<string, Promise<FBlob>>} */
2023-03-15 22:44:08 +01:00
this.blobPromises = {};
this.addResp(resp);
}
2020-08-26 16:50:16 +02:00
async loadSubTree(subTreeNoteId) {
const resp = await server.get(`tree?subTreeNoteId=${subTreeNoteId}`);
2020-08-26 16:50:16 +02:00
this.addResp(resp);
return this.notes[subTreeNoteId];
}
addResp(resp) {
const noteRows = resp.notes;
const branchRows = resp.branches;
const attributeRows = resp.attributes;
const noteIdsToSort = new Set();
2019-10-26 09:51:08 +02:00
for (const noteRow of noteRows) {
const {noteId} = noteRow;
let note = this.notes[noteId];
2019-10-26 09:51:08 +02:00
if (note) {
note.update(noteRow);
2019-10-26 09:51:08 +02:00
2023-05-05 23:41:11 +02:00
// search note doesn't have child branches in the database and all the children are virtual branches
if (note.type !== 'search') {
for (const childNoteId of note.children) {
const childNote = this.notes[childNoteId];
2019-10-26 09:51:08 +02:00
if (childNote) {
childNote.parents = childNote.parents.filter(p => p !== noteId);
delete this.branches[childNote.parentToBranch[noteId]];
delete childNote.parentToBranch[noteId];
}
2019-10-26 09:51:08 +02:00
}
note.children = [];
2020-12-17 21:19:52 +01:00
note.childToBranch = {};
2019-10-26 09:51:08 +02:00
}
2019-04-13 22:10:16 +02:00
// we want to remove all "real" branches (represented in the database) since those will be created
// from branches argument but want to preserve all virtual ones from saved search
note.parents = note.parents.filter(parentNoteId => {
2019-10-26 09:51:08 +02:00
const parentNote = this.notes[parentNoteId];
const branch = this.branches[parentNote.childToBranch[noteId]];
2019-04-13 22:10:16 +02:00
if (!parentNote || !branch) {
return false;
}
2019-04-13 22:10:16 +02:00
if (branch.fromSearchNote) {
return true;
2019-10-26 09:51:08 +02:00
}
2019-04-13 22:10:16 +02:00
parentNote.children = parentNote.children.filter(p => p !== noteId);
2019-04-13 22:10:16 +02:00
delete this.branches[parentNote.childToBranch[noteId]];
delete parentNote.childToBranch[noteId];
return false;
});
}
else {
this.notes[noteId] = new FNote(this, noteRow);
}
2020-01-31 20:52:31 +01:00
}
2019-04-13 22:10:16 +02:00
2020-01-31 20:52:31 +01:00
for (const branchRow of branchRows) {
const branch = new FBranch(this, branchRow);
2019-04-13 22:10:16 +02:00
2020-01-31 20:52:31 +01:00
this.branches[branch.branchId] = branch;
const childNote = this.notes[branch.noteId];
if (childNote) {
2023-02-28 23:23:17 +01:00
childNote.addParent(branch.parentNoteId, branch.branchId, false);
2019-10-26 09:51:08 +02:00
}
2020-01-31 20:52:31 +01:00
const parentNote = this.notes[branch.parentNoteId];
2020-01-31 20:52:31 +01:00
if (parentNote) {
parentNote.addChild(branch.noteId, branch.branchId, false);
noteIdsToSort.add(parentNote.noteId);
2019-10-26 09:51:08 +02:00
}
}
for (const attributeRow of attributeRows) {
const {attributeId} = attributeRow;
this.attributes[attributeId] = new FAttribute(this, attributeRow);
const note = this.notes[attributeRow.noteId];
2020-09-05 22:45:26 +02:00
if (note && !note.attributes.includes(attributeId)) {
note.attributes.push(attributeId);
}
if (attributeRow.type === 'relation') {
const targetNote = this.notes[attributeRow.value];
if (targetNote) {
2020-02-25 16:31:44 +01:00
if (!targetNote.targetRelations.includes(attributeId)) {
targetNote.targetRelations.push(attributeId);
}
}
}
}
// sort all of them at once, this avoids repeated sorts (#1480)
for (const noteId of noteIdsToSort) {
this.notes[noteId].sortChildren();
2023-02-28 23:23:17 +01:00
this.notes[noteId].sortParents();
}
2019-10-26 09:51:08 +02:00
}
2020-01-26 11:41:40 +01:00
async reloadNotes(noteIds) {
if (noteIds.length === 0) {
return;
}
2020-01-29 20:14:02 +01:00
noteIds = Array.from(new Set(noteIds)); // make noteIds unique
2019-10-26 09:51:08 +02:00
const resp = await server.post('tree/load', { noteIds });
this.addResp(resp);
appContext.triggerEvent('notesReloaded', {noteIds});
}
async loadSearchNote(noteId) {
const note = await this.getNote(noteId);
2019-11-16 19:07:32 +01:00
if (!note || note.type !== 'search') {
return;
}
const {searchResultNoteIds, highlightedTokens, error} = await server.get(`search-note/${note.noteId}`);
if (!Array.isArray(searchResultNoteIds)) {
2022-04-19 23:36:21 +02:00
throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`);
}
2020-11-26 23:00:27 +01:00
// reset all the virtual branches from old search results
2021-04-16 22:57:37 +02:00
if (note.noteId in froca.notes) {
froca.notes[note.noteId].children = [];
froca.notes[note.noteId].childToBranch = {};
}
2021-12-20 17:30:47 +01:00
const branches = [...note.getParentBranches(), ...note.getChildBranches()];
2021-07-02 23:22:10 +02:00
searchResultNoteIds.forEach((resultNoteId, index) => branches.push({
// branchId should be repeatable since sometimes we reload some notes without rerendering the tree
branchId: `virt-${note.noteId}-${resultNoteId}`,
noteId: resultNoteId,
parentNoteId: note.noteId,
notePosition: (index + 1) * 10,
fromSearchNote: true
}));
// update this note with standard (parent) branches + virtual (children) branches
this.addResp({
notes: [note],
branches,
attributes: []
});
2021-04-16 22:57:37 +02:00
froca.notes[note.noteId].searchResultsLoaded = true;
froca.notes[note.noteId].highlightedTokens = highlightedTokens;
return {error};
}
/** @returns {FNote[]} */
getNotesFromCache(noteIds, silentNotFoundError = false) {
return noteIds.map(noteId => {
if (!this.notes[noteId] && !silentNotFoundError) {
2023-01-03 21:30:49 +01:00
console.trace(`Can't find note '${noteId}'`);
return null;
}
else {
return this.notes[noteId];
}
}).filter(note => !!note);
}
/** @returns {Promise<FNote[]>} */
async getNotes(noteIds, silentNotFoundError = false) {
noteIds = Array.from(new Set(noteIds)); // make unique
2019-12-16 22:00:44 +01:00
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
2020-01-26 11:41:40 +01:00
await this.reloadNotes(missingNoteIds);
return noteIds.map(noteId => {
if (!this.notes[noteId] && !silentNotFoundError) {
2023-01-03 21:30:49 +01:00
console.trace(`Can't find note '${noteId}'`);
2018-08-06 11:30:37 +02:00
2018-08-12 12:59:38 +02:00
return null;
2020-11-22 23:05:02 +01:00
} else {
return this.notes[noteId];
}
}).filter(note => !!note);
}
/** @returns {Promise<boolean>} */
2019-04-13 22:10:16 +02:00
async noteExists(noteId) {
const notes = await this.getNotes([noteId], true);
return notes.length === 1;
}
/** @returns {Promise<FNote>} */
2019-09-08 11:25:57 +02:00
async getNote(noteId, silentNotFoundError = false) {
2018-05-26 16:16:34 -04:00
if (noteId === 'none') {
2020-05-03 13:52:12 +02:00
console.trace(`No 'none' note.`);
return null;
}
else if (!noteId) {
2022-04-19 23:36:21 +02:00
console.trace(`Falsy noteId '${noteId}', returning null.`);
2018-05-26 16:16:34 -04:00
return null;
}
2019-09-08 11:25:57 +02:00
return (await this.getNotes([noteId], silentNotFoundError))[0];
}
/** @returns {FNote|null} */
2020-01-15 21:36:01 +01:00
getNoteFromCache(noteId) {
2020-12-13 20:13:57 +01:00
if (!noteId) {
throw new Error("Empty noteId");
}
2020-01-15 21:36:01 +01:00
return this.notes[noteId];
}
/** @returns {FBranch[]} */
getBranches(branchIds, silentNotFoundError = false) {
return branchIds
.map(branchId => this.getBranch(branchId, silentNotFoundError))
.filter(b => !!b);
}
/** @returns {FBranch} */
getBranch(branchId, silentNotFoundError = false) {
if (!(branchId in this.branches)) {
if (!silentNotFoundError) {
2023-01-03 21:30:49 +01:00
logError(`Not existing branch '${branchId}'`);
}
}
2019-10-26 20:48:56 +02:00
else {
return this.branches[branchId];
}
}
2020-01-21 21:43:23 +01:00
async getBranchId(parentNoteId, childNoteId) {
2020-09-19 22:47:14 +02:00
if (childNoteId === 'root') {
2023-01-03 21:30:49 +01:00
return 'none_root';
2020-09-19 22:47:14 +02:00
}
2020-01-21 21:43:23 +01:00
const child = await this.getNote(childNoteId);
if (!child) {
2023-01-03 21:30:49 +01:00
logError(`Could not find branchId for parent '${parentNoteId}', child '${childNoteId}' since child does not exist`);
return null;
}
2020-01-21 21:43:23 +01:00
return child.parentToBranch[parentNoteId];
}
2020-01-26 10:42:24 +01:00
2023-05-29 00:19:54 +02:00
/** @returns {Promise<FAttachment>} */
2023-06-30 15:25:45 +02:00
async getAttachment(attachmentId, silentNotFoundError = false) {
2023-05-29 00:19:54 +02:00
const attachment = this.attachments[attachmentId];
if (attachment) {
return attachment;
}
// load all attachments for the given note even if one is requested, don't load one by one
2023-06-30 15:25:45 +02:00
let attachmentRows;
try {
attachmentRows = await server.getWithSilentNotFound(`attachments/${attachmentId}/all`);
2023-06-30 15:25:45 +02:00
}
catch (e) {
if (silentNotFoundError) {
logInfo(`Attachment '${attachmentId} not found, but silentNotFoundError is enabled: ` + e.message);
return null;
2023-06-30 15:25:45 +02:00
} else {
throw e;
}
}
2023-05-29 00:19:54 +02:00
const attachments = this.processAttachmentRows(attachmentRows);
if (attachments.length) {
attachments[0].getNote().attachments = attachments;
}
2023-04-17 23:21:28 +02:00
2023-05-29 00:19:54 +02:00
return this.attachments[attachmentId];
}
/** @returns {Promise<FAttachment[]>} */
async getAttachmentsForNote(noteId) {
const attachmentRows = await server.get(`notes/${noteId}/attachments`);
return this.processAttachmentRows(attachmentRows);
}
/** @returns {FAttachment[]} */
processAttachmentRows(attachmentRows) {
return attachmentRows.map(attachmentRow => {
let attachment;
if (attachmentRow.attachmentId in this.attachments) {
attachment = this.attachments[attachmentRow.attachmentId];
attachment.update(attachmentRow);
} else {
attachment = new FAttachment(this, attachmentRow);
this.attachments[attachment.attachmentId] = attachment;
}
return attachment;
});
2023-04-17 23:21:28 +02:00
}
2023-05-05 22:21:51 +02:00
/** @returns {Promise<FBlob>} */
2023-05-05 16:37:39 +02:00
async getBlob(entityType, entityId, opts = {}) {
2023-05-05 22:21:51 +02:00
opts.preview = !!opts.preview;
const key = `${entityType}-${entityId}-${opts.preview}`;
2023-05-05 16:37:39 +02:00
if (!this.blobPromises[key]) {
2023-05-05 22:21:51 +02:00
this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?preview=${opts.preview}`)
2023-05-05 16:37:39 +02:00
.then(row => new FBlob(row))
.catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`));
2023-01-03 21:30:49 +01:00
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
// if the blob is updated within the cache lifetime, it should be invalidated by froca_updater
2023-05-05 16:37:39 +02:00
this.blobPromises[key].then(
() => setTimeout(() => this.blobPromises[key] = null, 1000)
);
}
2023-05-05 16:37:39 +02:00
return await this.blobPromises[key];
}
}
2021-04-16 22:57:37 +02:00
const froca = new Froca();
2021-04-16 22:57:37 +02:00
export default froca;