/* This file is part of the Notesnook project (https://notesnook.com/) Copyright (C) 2023 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import Notes from "../collections/notes"; import Storage from "../database/storage"; import FileStorage from "../database/fs"; import Notebooks from "../collections/notebooks"; import Trash from "../collections/trash"; import Tags from "../collections/tags"; import Sync from "./sync"; import Vault from "./vault"; import Lookup from "./lookup"; import Content from "../collections/content"; import Backup from "../database/backup"; import Session from "./session"; import Constants from "../utils/constants"; import { EV, EVENTS } from "../common"; import Settings from "./settings"; import Migrations from "./migrations"; import UserManager from "./user-manager"; import http from "../utils/http"; import Monographs from "./monographs"; import Offers from "./offers"; import Attachments from "../collections/attachments"; import Debug from "./debug"; import { Mutex } from "async-mutex"; import NoteHistory from "../collections/note-history"; import MFAManager from "./mfa-manager"; import EventManager from "../utils/event-manager"; import Pricing from "./pricing"; import { logger } from "../logger"; import Shortcuts from "../collections/shortcuts"; import Reminders from "../collections/reminders"; import Relations from "../collections/relations"; import Subscriptions from "./subscriptions"; /** * @type {EventSource} */ var NNEventSource; // const DIFFERENCE_THRESHOLD = 20 * 1000; // const MAX_TIME_ERROR_FAILURES = 5; class Database { isInitialized = false; /** * * @param {any} storage * @param {EventSource} eventsource */ constructor() { /** * @type {EventSource} */ this.evtSource = null; this.sseMutex = new Mutex(); this.lastHeartbeat = undefined; // { local: 0, server: 0 }; this.timeErrorFailures = 0; this.eventManager = new EventManager(); } setup(storage, eventsource, fs, compressor) { this.compressor = compressor; this.storage = storage ? new Storage(storage) : null; this.fs = fs && storage ? new FileStorage(fs, storage) : null; NNEventSource = eventsource; this.session = new Session(this.storage); this.user = new UserManager(this.storage, this); this.mfa = new MFAManager(this.storage, this); this.syncer = new Sync(this); this.vault = new Vault(this); this.lookup = new Lookup(this); this.backup = new Backup(this); this.settings = new Settings(this); this.migrations = new Migrations(this); this.monographs = new Monographs(this); this.offers = new Offers(); this.debug = new Debug(); this.pricing = new Pricing(); this.subscriptions = new Subscriptions(this.user.tokenManager); this.trash = new Trash(this); } async _validate() { if (!(await this.session.valid())) { throw new Error( "Your system clock is not setup correctly. Please adjust your date and time and then retry." ); } await this.session.set(); } async init() { EV.subscribeMulti( [EVENTS.userLoggedIn, EVENTS.userFetched, EVENTS.tokenRefreshed], this.connectSSE, this ); EV.subscribe(EVENTS.attachmentDeleted, async (attachment) => { await this.fs.cancel(attachment.metadata?.hash); }); EV.subscribe(EVENTS.userLoggedOut, async () => { await this.monographs.deinit(); await this.fs.clear(); this.disconnectSSE(); }); await this._validate(); await this.initCollections(); await this.migrations.init(); this.isInitialized = true; if (this.migrations.required()) { logger.warn("Database migration is required."); } } async initCollections() { await this.settings.init(); // collections /** @type {Notebooks} */ this.notebooks = await Notebooks.new(this, "notebooks"); /** @type {Tags} */ this.tags = await Tags.new(this, "tags"); /** @type {Tags} */ this.colors = await Tags.new(this, "colors"); /** @type {Content} */ this.content = await Content.new(this, "content", false); /** @type {Attachments} */ this.attachments = await Attachments.new(this, "attachments"); /**@type {NoteHistory} */ this.noteHistory = await NoteHistory.new(this, "notehistory", false); /**@type {Shortcuts} */ this.shortcuts = await Shortcuts.new(this, "shortcuts"); /**@type {Reminders} */ this.reminders = await Reminders.new(this, "reminders"); /**@type {Relations} */ this.relations = await Relations.new(this, "relations"); /** @type {Notes} */ this.notes = await Notes.new(this, "notes"); await this.trash.init(); this.monographs.init().catch(console.error); } disconnectSSE() { if (!this.evtSource) return; this.evtSource.onopen = null; this.evtSource.onmessage = null; this.evtSource.onerror = null; this.evtSource.close(); this.evtSource = null; } /** * * @param {{force: boolean, error: any}} args */ async connectSSE(args) { if (args && !!args.error) return; await this.sseMutex.runExclusive(async () => { this.eventManager.publish(EVENTS.databaseSyncRequested, true, false); const forceReconnect = args && args.force; if ( !NNEventSource || (!forceReconnect && this.evtSource && this.evtSource.readyState === this.evtSource.OPEN) ) return; this.disconnectSSE(); const token = await this.user.tokenManager.getAccessToken(); if (!token) return; this.evtSource = new NNEventSource(`${Constants.SSE_HOST}/sse`, { headers: { Authorization: `Bearer ${token}` } }); this.evtSource.onopen = async () => { console.log("SSE: opened channel successfully!"); }; this.evtSource.onerror = function (error) { console.log("SSE: error:", error); }; this.evtSource.onmessage = async (event) => { try { var { type, data } = JSON.parse(event.data); data = JSON.parse(data); } catch (e) { console.log("SSE: Unsupported message. Message = ", event.data); return; } switch (type) { // TODO: increase reliablity for this. // case "heartbeat": { // const { t: serverTime } = data; // const localTime = Date.now(); // if (!this.lastHeartbeat) { // this.lastHeartbeat = { local: localTime, server: serverTime }; // break; // } // const timeElapsed = { // local: localTime - this.lastHeartbeat.local, // server: serverTime - this.lastHeartbeat.server, // }; // const travelTime = timeElapsed.local - timeElapsed.server; // const actualTime = localTime - travelTime; // const diff = actualTime - serverTime; // // Fail several times consecutively before raising an error. This is done to root out // // false positives. // if (Math.abs(diff) > DIFFERENCE_THRESHOLD) { // if (this.timeErrorFailures >= MAX_TIME_ERROR_FAILURES) { // EV.publish(EVENTS.systemTimeInvalid, { serverTime, localTime }); // } else this.timeErrorFailures++; // } else this.timeErrorFailures = 0; // this.lastHeartbeat.local = localTime; // this.lastHeartbeat.server = serverTime; // break; // } case "upgrade": { const user = await this.user.getUser(); user.subscription = data; await this.user.setUser(user); EV.publish(EVENTS.userSubscriptionUpdated, data); break; } case "logout": { await this.user.logout(true, data.reason || "Unknown."); break; } case "emailConfirmed": { const token = await this.storage.read("token"); await this.user.tokenManager._refreshToken(token); await this.user.fetchUser(true); EV.publish(EVENTS.userEmailConfirmed); break; } } }; }); } async lastSynced() { return (await this.storage.read("lastSynced")) || 0; } /** * * @param {{ * type: "full" | "fetch" | "send"; * force?: boolean; * serverLastSynced?: number; * }} options * @returns */ sync(options) { return this.syncer.start(options); } /** * * @param {{AUTH_HOST: string, API_HOST: string, SSE_HOST: string, SUBSCRIPTIONS_HOST: string, ISSUES_HOST: string}} hosts */ host(hosts) { if (process.env.NODE_ENV !== "production") { Constants.AUTH_HOST = hosts.AUTH_HOST || Constants.AUTH_HOST; Constants.API_HOST = hosts.API_HOST || Constants.API_HOST; Constants.SSE_HOST = hosts.SSE_HOST || Constants.SSE_HOST; Constants.SUBSCRIPTIONS_HOST = hosts.SUBSCRIPTIONS_HOST || Constants.SUBSCRIPTIONS_HOST; Constants.ISSUES_HOST = hosts.ISSUES_HOST || Constants.ISSUES_HOST; } } version() { return http.get(`${Constants.API_HOST}/version`); } async announcements() { let url = `${Constants.API_HOST}/announcements/active`; const user = await this.user.getUser(); if (user) url += `?userId=${user.id}`; return http.get(url); } } export default Database;