Changeset View
Changeset View
Standalone View
Standalone View
web/database/database-module-provider.js
// @flow | // @flow | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import localforage from 'localforage'; | import localforage from 'localforage'; | ||||
import { | import { | ||||
DATABASE_WORKER_PATH, | DATABASE_WORKER_PATH, | ||||
DATABASE_MODULE_FILE_PATH, | DATABASE_MODULE_FILE_PATH, | ||||
SQLITE_ENCRYPTION_KEY, | SQLITE_ENCRYPTION_KEY, | ||||
} from './utils/constants.js'; | } from './utils/constants.js'; | ||||
import { isDesktopSafari, isSQLiteSupported } from './utils/db-utils.js'; | import { isDesktopSafari, isSQLiteSupported } from './utils/db-utils.js'; | ||||
import { | import { | ||||
exportKeyToJWK, | exportKeyToJWK, | ||||
generateDatabaseCryptoKey, | generateDatabaseCryptoKey, | ||||
} from './utils/worker-crypto-utils.js'; | } from './utils/worker-crypto-utils.js'; | ||||
import WorkerConnectionProxy from './utils/WorkerConnectionProxy.js'; | import WorkerConnectionProxy from './utils/WorkerConnectionProxy.js'; | ||||
import type { AppState } from '../redux/redux-setup.js'; | |||||
import { | import { | ||||
workerRequestMessageTypes, | workerRequestMessageTypes, | ||||
type WorkerRequestMessage, | type WorkerRequestMessage, | ||||
type WorkerResponseMessage, | type WorkerResponseMessage, | ||||
} from '../types/worker-types.js'; | } from '../types/worker-types.js'; | ||||
declare var commQueryExecutorFilename: string; | declare var commQueryExecutorFilename: string; | ||||
declare var preloadedState: AppState; | |||||
const databaseStatuses = Object.freeze({ | const databaseStatuses = Object.freeze({ | ||||
notSupported: 'NOT_SUPPORTED', | notRunning: 'NOT_RUNNING', | ||||
initSuccess: 'INIT_SUCCESS', | initSuccess: 'INIT_SUCCESS', | ||||
initInProgress: 'INIT_IN_PROGRESS', | initInProgress: 'INIT_IN_PROGRESS', | ||||
initError: 'INIT_ERROR', | initError: 'INIT_ERROR', | ||||
}); | }); | ||||
type DatabaseStatus = $Values<typeof databaseStatuses>; | type DatabaseStatus = $Values<typeof databaseStatuses>; | ||||
type InitOptions = { +clearDatabase: boolean }; | |||||
class DatabaseModule { | class DatabaseModule { | ||||
worker: ?SharedWorker; | worker: ?SharedWorker; | ||||
workerProxy: ?WorkerConnectionProxy; | workerProxy: ?WorkerConnectionProxy; | ||||
initPromise: ?Promise<void>; | initPromise: ?Promise<void>; | ||||
status: DatabaseStatus = databaseStatuses.notSupported; | status: DatabaseStatus = databaseStatuses.notRunning; | ||||
async init(currentLoggedInUserID: ?string): Promise<void> { | async init({ clearDatabase }: InitOptions): Promise<void> { | ||||
if (!currentLoggedInUserID) { | if (!isSQLiteSupported()) { | ||||
console.warn('SQLite is not supported'); | |||||
this.status = databaseStatuses.initError; | |||||
return; | return; | ||||
} | } | ||||
if (!isSQLiteSupported(currentLoggedInUserID)) { | if (clearDatabase && this.status === databaseStatuses.initSuccess) { | ||||
console.warn('Sqlite is not supported'); | console.info('Clearing sensitive data'); | ||||
this.status = databaseStatuses.notSupported; | invariant(this.workerProxy, 'Worker proxy should exist'); | ||||
return; | await this.workerProxy.scheduleOnWorker({ | ||||
type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA, | |||||
}); | |||||
this.status = databaseStatuses.notRunning; | |||||
} | } | ||||
if (this.status === databaseStatuses.initInProgress) { | if (this.status === databaseStatuses.initInProgress) { | ||||
await this.initPromise; | await this.initPromise; | ||||
return; | return; | ||||
} | } | ||||
if ( | if ( | ||||
Show All 34 Lines | this.initPromise = (async () => { | ||||
this.status = databaseStatuses.initError; | this.status = databaseStatuses.initError; | ||||
console.error(`Database initialization failure`, error); | console.error(`Database initialization failure`, error); | ||||
} | } | ||||
})(); | })(); | ||||
await this.initPromise; | await this.initPromise; | ||||
} | } | ||||
async clearSensitiveData(): Promise<void> { | |||||
this.status = databaseStatuses.notSupported; | |||||
invariant(this.workerProxy, 'Worker proxy should exist'); | |||||
await this.workerProxy.scheduleOnWorker({ | |||||
type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA, | |||||
}); | |||||
} | |||||
async isDatabaseSupported(): Promise<boolean> { | async isDatabaseSupported(): Promise<boolean> { | ||||
if (this.status === databaseStatuses.initInProgress) { | if (this.status === databaseStatuses.initInProgress) { | ||||
await this.initPromise; | await this.initPromise; | ||||
} | } | ||||
return this.status === databaseStatuses.initSuccess; | return this.status === databaseStatuses.initSuccess; | ||||
} | } | ||||
async schedule( | async schedule( | ||||
payload: WorkerRequestMessage, | payload: WorkerRequestMessage, | ||||
): Promise<?WorkerResponseMessage> { | ): Promise<?WorkerResponseMessage> { | ||||
if (this.status === databaseStatuses.notSupported) { | if (this.status === databaseStatuses.notRunning) { | ||||
throw new Error('Database not supported'); | throw new Error('Database not running'); | ||||
} | } | ||||
if (this.status === databaseStatuses.initInProgress) { | if (this.status === databaseStatuses.initInProgress) { | ||||
await this.initPromise; | await this.initPromise; | ||||
} | } | ||||
if (this.status === databaseStatuses.initError) { | if (this.status === databaseStatuses.initError) { | ||||
throw new Error('Database could not be initialized'); | throw new Error('Database could not be initialized'); | ||||
Show All 15 Lines | async function getSafariEncryptionKey(): Promise<SubtleCrypto$JsonWebKey> { | ||||
await localforage.setItem(SQLITE_ENCRYPTION_KEY, newEncryptionKey); | await localforage.setItem(SQLITE_ENCRYPTION_KEY, newEncryptionKey); | ||||
return await exportKeyToJWK(newEncryptionKey); | return await exportKeyToJWK(newEncryptionKey); | ||||
} | } | ||||
let databaseModule: ?DatabaseModule = null; | let databaseModule: ?DatabaseModule = null; | ||||
async function getDatabaseModule(): Promise<DatabaseModule> { | async function getDatabaseModule(): Promise<DatabaseModule> { | ||||
if (!databaseModule) { | if (!databaseModule) { | ||||
databaseModule = new DatabaseModule(); | databaseModule = new DatabaseModule(); | ||||
const currentLoggedInUserID = preloadedState.currentUserInfo?.anonymous | await databaseModule.init({ clearDatabase: false }); | ||||
? undefined | |||||
: preloadedState.currentUserInfo?.id; | |||||
await databaseModule.init(currentLoggedInUserID); | |||||
} | } | ||||
return databaseModule; | return databaseModule; | ||||
} | } | ||||
// Start initializing the database immediately | |||||
getDatabaseModule(); | |||||
export { getDatabaseModule }; | export { getDatabaseModule }; |