From 3c575c01ad78b7a7ac8a7884c40bc583b986c394 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 5 Aug 2023 11:16:01 +0500 Subject: [PATCH] web: fix storage for Firefox & Tor private mode --- apps/web/src/common/db.ts | 2 +- apps/web/src/index.tsx | 3 +- apps/web/src/interfaces/key-value.ts | 61 ++++++++++++++++------------ apps/web/src/interfaces/storage.ts | 21 ++++++---- apps/web/src/utils/logger.ts | 4 +- 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/apps/web/src/common/db.ts b/apps/web/src/common/db.ts index d008462e8..50026fa08 100644 --- a/apps/web/src/common/db.ts +++ b/apps/web/src/common/db.ts @@ -42,7 +42,7 @@ async function initializeDatabase(persistence: DatabasePersistence) { }); database.setup( - new NNStorage("Notesnook", persistence), + await NNStorage.createInstance("Notesnook", persistence), EventSource, FS, new Compressor() diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 15b1ae4b3..fe8d5d9aa 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -28,8 +28,6 @@ import { initalizeLogger, logger } from "./utils/logger"; import { AuthProps } from "./views/auth"; import { loadDatabase } from "./hooks/use-database"; -initalizeLogger(); - type Route = { component: () => Promise<{ default: TProps extends null @@ -150,6 +148,7 @@ function isSessionExpired(path: Routes): RouteWithPath | null { renderApp(); async function renderApp() { + await initalizeLogger(); const { path, route: { component, props } diff --git a/apps/web/src/interfaces/key-value.ts b/apps/web/src/interfaces/key-value.ts index 1884f88ea..a0a7f401f 100644 --- a/apps/web/src/interfaces/key-value.ts +++ b/apps/web/src/interfaces/key-value.ts @@ -174,29 +174,25 @@ export type UseStore = ( export class IndexedDBKVStore implements IKVStore { store: UseStore; - static isIndexedDBSupported(): boolean { - return "indexedDB" in window; + static async isIndexedDBSupported(): Promise { + if (!("indexedDB" in window)) return false; + try { + await promisifyIDBRequest(indexedDB.open("checkIDBSupport")); + return true; + } catch { + console.error("IndexedDB is not supported in this browser."); + return false; + } } constructor(databaseName: string, storeName: string) { this.store = this.createStore(databaseName, storeName); } - private promisifyRequest( - request: IDBRequest | IDBTransaction - ): Promise { - return new Promise((resolve, reject) => { - // @ts-ignore - file size hacks - request.oncomplete = request.onsuccess = () => resolve(request.result); - // @ts-ignore - file size hacks - request.onabort = request.onerror = () => reject(request.error); - }); - } - private createStore(dbName: string, storeName: string): UseStore { const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); - const dbp = this.promisifyRequest(request); + const dbp = promisifyIDBRequest(request); return (txMode, callback) => dbp.then((db) => @@ -213,26 +209,26 @@ export class IndexedDBKVStore implements IKVStore { callback(this.result); this.result.continue(); }; - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); } get(key: string): Promise { return this.store("readonly", (store) => - this.promisifyRequest(store.get(key)) + promisifyIDBRequest(store.get(key)) ); } set(key: string, value: any): Promise { return this.store("readwrite", (store) => { store.put(value, key); - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); }); } setMany(entries: [string, any][]): Promise { return this.store("readwrite", (store) => { entries.forEach((entry) => store.put(entry[1], entry[0])); - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); }); } @@ -241,7 +237,7 @@ export class IndexedDBKVStore implements IKVStore { Promise.all( keys.map(async (key) => [ key, - await this.promisifyRequest(store.get(key)) + await promisifyIDBRequest(store.get(key)) ]) ) ); @@ -250,21 +246,21 @@ export class IndexedDBKVStore implements IKVStore { delete(key: string): Promise { return this.store("readwrite", (store) => { store.delete(key); - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); }); } deleteMany(keys: string[]): Promise { return this.store("readwrite", (store: IDBObjectStore) => { keys.forEach((key: IDBValidKey) => store.delete(key)); - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); }); } clear(): Promise { return this.store("readwrite", (store) => { store.clear(); - return this.promisifyRequest(store.transaction); + return promisifyIDBRequest(store.transaction); }); } @@ -272,7 +268,7 @@ export class IndexedDBKVStore implements IKVStore { return this.store("readonly", (store) => { // Fast path for modern browsers if (store.getAllKeys) { - return this.promisifyRequest( + return promisifyIDBRequest( store.getAllKeys() as unknown as IDBRequest ); } @@ -289,7 +285,7 @@ export class IndexedDBKVStore implements IKVStore { return this.store("readonly", (store) => { // Fast path for modern browsers if (store.getAll) { - return this.promisifyRequest(store.getAll() as IDBRequest); + return promisifyIDBRequest(store.getAll() as IDBRequest); } const items: T[] = []; @@ -308,10 +304,10 @@ export class IndexedDBKVStore implements IKVStore { // (although, hopefully we'll get a simpler path some day) if (store.getAll && store.getAllKeys) { return Promise.all([ - this.promisifyRequest( + promisifyIDBRequest( store.getAllKeys() as unknown as IDBRequest ), - this.promisifyRequest(store.getAll() as IDBRequest) + promisifyIDBRequest(store.getAll() as IDBRequest) ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]])); } @@ -325,3 +321,16 @@ export class IndexedDBKVStore implements IKVStore { }); } } + +function promisifyIDBRequest( + request: IDBRequest | IDBTransaction +): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - file size hacks + request.onabort = request.onerror = () => reject(request.error); + }); +} diff --git a/apps/web/src/interfaces/storage.ts b/apps/web/src/interfaces/storage.ts index 8d7af35a6..2974f7b73 100644 --- a/apps/web/src/interfaces/storage.ts +++ b/apps/web/src/interfaces/storage.ts @@ -31,17 +31,21 @@ export type DatabasePersistence = "memory" | "db"; const APP_SALT = "oVzKtazBo7d8sb7TBvY9jw"; - export class NNStorage { - database: IKVStore; + database!: IKVStore; - constructor(name: string, persistence: DatabasePersistence = "db") { - this.database = + static async createInstance( + name: string, + persistence: DatabasePersistence = "db" + ) { + const storage = new NNStorage(); + storage.database = persistence === "memory" ? new MemoryKVStore() - : IndexedDBKVStore.isIndexedDBSupported() + : (await IndexedDBKVStore.isIndexedDBSupported()) ? new IndexedDBKVStore(name, "keyvaluepairs") : new LocalStorageKVStore(); + return storage; } read(key: string): Promise { @@ -78,7 +82,7 @@ export class NNStorage { const keyData = await crypto.exportKey(password, salt); if ( - IndexedDBKVStore.isIndexedDBSupported() && + (await IndexedDBKVStore.isIndexedDBSupported()) && window?.crypto?.subtle && keyData.key ) { @@ -94,7 +98,10 @@ export class NNStorage { } async getCryptoKey(name: string): Promise { - if (IndexedDBKVStore.isIndexedDBSupported() && window?.crypto?.subtle) { + if ( + (await IndexedDBKVStore.isIndexedDBSupported()) && + window?.crypto?.subtle + ) { const pbkdfKey = await this.read(name); const cipheredKey = await this.read(`${name}@_k`); if (typeof cipheredKey === "string") return cipheredKey; diff --git a/apps/web/src/utils/logger.ts b/apps/web/src/utils/logger.ts index 121c3b4de..084f57d55 100644 --- a/apps/web/src/utils/logger.ts +++ b/apps/web/src/utils/logger.ts @@ -28,8 +28,8 @@ import { DatabasePersistence, NNStorage } from "../interfaces/storage"; import { zip } from "./zip"; let logger: typeof _logger; -function initalizeLogger(persistence: DatabasePersistence = "db") { - initalize(new NNStorage("Logs", persistence)); +async function initalizeLogger(persistence: DatabasePersistence = "db") { + initalize(await NNStorage.createInstance("Logs", persistence)); logger = _logger.scope("notesnook-web"); }