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 Outbox from "./outbox"; 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"; /** * @type {EventSource} */ var NNEventSource; const DIFFERENCE_THRESHOLD = 20 * 1000; const MAX_TIME_ERROR_FAILURES = 5; class Database { /** * * @param {any} storage * @param {EventSource} eventsource */ constructor(storage, eventsource, fs) { /** * @type {EventSource} */ this.evtSource = null; this.sseMutex = new Mutex(); this.lastHeartbeat = undefined; // { local: 0, server: 0 }; this.timeErrorFailures = 0; this.eventManager = new EventManager(); this.storage = new Storage(storage); this.fs = new FileStorage(fs, storage); NNEventSource = eventsource; } 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(); }); EV.subscribe(EVENTS.databaseCollectionInitiated, async (collectionName) => { switch (collectionName) { case "notes": { await this.monographs.init(); await this.trash.init(); break; } } }); this.session = new Session(this.storage); await this._validate(); 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.outbox = new Outbox(this); this.monographs = new Monographs(this); this.offers = new Offers(); this.debug = new Debug(); this.pricing = new Pricing(); // collections /** @type {Notes} */ this.notes = await Notes.new(this, "notes", true, true); /** @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); this.trash = new Trash(this); await this.settings.init(); await this.outbox.init(); await this.user.init(); await this.migrations.init(); await this.migrations.migrate(); } 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) { await this.sseMutex.runExclusive(async () => { if (args && !!args.error) return; 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(); let token = await this.user.tokenManager.getAccessToken(); 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) { 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 "userDeleted": { await this.user.logout(false, "Account Deleted"); break; } case "userPasswordChanged": { await this.user.logout(true, "Password Changed"); 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; } sync(full = true, force = false) { return this.syncer.start(full, force); } /** * * @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;