diff --git a/web/database/database-module-provider.js b/web/database/database-module-provider.js index a1da53664..be3956f01 100644 --- a/web/database/database-module-provider.js +++ b/web/database/database-module-provider.js @@ -1,153 +1,160 @@ // @flow +import invariant from 'invariant'; import localforage from 'localforage'; import { DATABASE_WORKER_PATH, DATABASE_MODULE_FILE_PATH, SQLITE_ENCRYPTION_KEY, } from './utils/constants.js'; import { isDesktopSafari, isSQLiteSupported } from './utils/db-utils.js'; import { exportKeyToJWK, generateDatabaseCryptoKey, } from './utils/worker-crypto-utils.js'; import WorkerConnectionProxy from './utils/WorkerConnectionProxy.js'; import type { AppState } from '../redux/redux-setup.js'; import { workerRequestMessageTypes, type WorkerRequestMessage, type WorkerResponseMessage, } from '../types/worker-types.js'; declare var commQueryExecutorFilename: string; declare var preloadedState: AppState; const databaseStatuses = Object.freeze({ notSupported: 'NOT_SUPPORTED', initSuccess: 'INIT_SUCCESS', initInProgress: 'INIT_IN_PROGRESS', initError: 'INIT_ERROR', }); type DatabaseStatus = $Values; class DatabaseModule { - worker: SharedWorker; - workerProxy: WorkerConnectionProxy; - initPromise: Promise; - status: DatabaseStatus; + worker: ?SharedWorker; + workerProxy: ?WorkerConnectionProxy; + initPromise: ?Promise; + status: DatabaseStatus = databaseStatuses.notSupported; - constructor() { - const currentLoggedInUserID = preloadedState.currentUserInfo?.anonymous - ? undefined - : preloadedState.currentUserInfo?.id; - const isSupported = isSQLiteSupported(currentLoggedInUserID); + async init(currentLoggedInUserID: ?string): Promise { + if (!currentLoggedInUserID) { + return; + } - if (!isSupported || isDesktopSafari) { + if (!isSQLiteSupported(currentLoggedInUserID)) { + console.warn('Sqlite is not supported'); this.status = databaseStatuses.notSupported; - } else { - this.init(); + return; + } + + if (this.status === databaseStatuses.initInProgress) { + await this.initPromise; + return; + } + + if ( + this.status === databaseStatuses.initSuccess || + this.status === databaseStatuses.initError + ) { + return; } - } - init(encryptionKey?: ?SubtleCrypto$JsonWebKey) { this.status = databaseStatuses.initInProgress; + + let encryptionKey = null; + if (isDesktopSafari) { + encryptionKey = await getSafariEncryptionKey(); + } + this.worker = new SharedWorker(DATABASE_WORKER_PATH); this.worker.onerror = console.error; this.workerProxy = new WorkerConnectionProxy( this.worker.port, console.error, ); const origin = window.location.origin; this.initPromise = (async () => { try { + invariant(this.workerProxy, 'Worker proxy should exist'); await this.workerProxy.scheduleOnWorker({ type: workerRequestMessageTypes.INIT, databaseModuleFilePath: `${origin}${DATABASE_MODULE_FILE_PATH}`, encryptionKey, commQueryExecutorFilename, }); this.status = databaseStatuses.initSuccess; console.info('Database initialization success'); } catch (error) { this.status = databaseStatuses.initError; console.error(`Database initialization failure`, error); } })(); - } - - async initDBForLoggedInUser(currentLoggedInUserID: ?string) { - if (this.status === databaseStatuses.initSuccess) { - return; - } - if ( - this.status === databaseStatuses.notSupported && - isSQLiteSupported(currentLoggedInUserID) - ) { - let encryptionKey = null; - if (isDesktopSafari) { - encryptionKey = await getSafariEncryptionKey(); - } - - this.init(encryptionKey); - } + await this.initPromise; } async clearSensitiveData(): Promise { this.status = databaseStatuses.notSupported; + invariant(this.workerProxy, 'Worker proxy should exist'); await this.workerProxy.scheduleOnWorker({ type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA, }); } async isDatabaseSupported(): Promise { if (this.status === databaseStatuses.initInProgress) { await this.initPromise; } return this.status === databaseStatuses.initSuccess; } async schedule( payload: WorkerRequestMessage, ): Promise { if (this.status === databaseStatuses.notSupported) { throw new Error('Database not supported'); } if (this.status === databaseStatuses.initInProgress) { await this.initPromise; } if (this.status === databaseStatuses.initError) { throw new Error('Database could not be initialized'); } + invariant(this.workerProxy, 'Worker proxy should exist'); return this.workerProxy.scheduleOnWorker(payload); } } async function getSafariEncryptionKey(): Promise { const encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); if (encryptionKey) { return await exportKeyToJWK(encryptionKey); } const newEncryptionKey = await generateDatabaseCryptoKey({ extractable: true, }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, newEncryptionKey); return await exportKeyToJWK(newEncryptionKey); } let databaseModule: ?DatabaseModule = null; async function getDatabaseModule(): Promise { if (!databaseModule) { databaseModule = new DatabaseModule(); + const currentLoggedInUserID = preloadedState.currentUserInfo?.anonymous + ? undefined + : preloadedState.currentUserInfo?.id; + await databaseModule.init(currentLoggedInUserID); } return databaseModule; } export { getDatabaseModule }; diff --git a/web/database/sqlite-data-handler.js b/web/database/sqlite-data-handler.js index ada418476..f98df6d3b 100644 --- a/web/database/sqlite-data-handler.js +++ b/web/database/sqlite-data-handler.js @@ -1,96 +1,96 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { getDatabaseModule } from './database-module-provider.js'; import { useSelector } from '../redux/redux-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; function SQLiteDataHandler(): React.Node { const dispatch = useDispatch(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const handleSensitiveData = React.useCallback(async () => { const databaseModule = await getDatabaseModule(); try { const currentUserData = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CURRENT_USER_ID, }); const currentDBUserID = currentUserData?.userID; if (currentDBUserID && currentDBUserID !== currentLoggedInUserID) { await databaseModule.clearSensitiveData(); } if ( currentLoggedInUserID && (currentDBUserID || currentDBUserID !== currentLoggedInUserID) ) { await databaseModule.schedule({ type: workerRequestMessageTypes.SET_CURRENT_USER_ID, userID: currentLoggedInUserID, }); } } catch (error) { console.error(error); throw error; } }, [currentLoggedInUserID]); React.useEffect(() => { (async () => { const databaseModule = await getDatabaseModule(); if (currentLoggedInUserID) { - await databaseModule.initDBForLoggedInUser(currentLoggedInUserID); + await databaseModule.init(currentLoggedInUserID); } if (!rehydrateConcluded) { return; } const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return; } await handleSensitiveData(); if (!currentLoggedInUserID) { return; } const data = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); if (!data?.store?.drafts && !data?.store?.reports) { return; } const reports = reportStoreOpsHandlers.translateClientDBData( data.store.reports, ); dispatch({ type: setClientDBStoreActionType, payload: { drafts: data.store.drafts, reports, }, }); })(); }, [ currentLoggedInUserID, dispatch, handleSensitiveData, rehydrateConcluded, ]); return null; } export { SQLiteDataHandler };