Skip to content

Instantly share code, notes, and snippets.

@MichalGniadek
Created September 13, 2023 16:23
Show Gist options
  • Save MichalGniadek/edafc0e8f8eeb730070d34f8b0492615 to your computer and use it in GitHub Desktop.
Save MichalGniadek/edafc0e8f8eeb730070d34f8b0492615 to your computer and use it in GitHub Desktop.
// @flow
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;
type InitializedDatabaseState =
| { type: 'init_error' }
| {
type: 'init_success',
worker: SharedWorker,
workerProxy: WorkerConnectionProxy,
};
type DatabaseState =
| { type: 'not_supported' }
| { type: 'init_in_progress', initPromise: Promise<InitializedDatabaseState> }
| InitializedDatabaseState;
class DatabaseModule {
state: DatabaseState = { type: 'not_supported' };
async init(currentLoggedInUserID: ?string): Promise<void> {
if (!currentLoggedInUserID) {
return;
}
if (!isSQLiteSupported(currentLoggedInUserID)) {
console.warn('Sqlite is not supported');
this.state = { type: 'not_supported' };
return;
}
if (this.state.type === 'init_in_progress') {
await this.state.initPromise;
return;
}
if (
this.state.type === 'init_success' ||
this.state.type === 'init_error'
) {
return;
}
this.state = {
type: 'init_in_progress',
initPromise: (async () => {
let encryptionKey = null;
if (isDesktopSafari) {
encryptionKey = await getSafariEncryptionKey();
}
const worker = new SharedWorker(DATABASE_WORKER_PATH);
worker.onerror = console.error;
const workerProxy = new WorkerConnectionProxy(
worker.port,
console.error,
);
const origin = window.location.origin;
try {
await workerProxy.scheduleOnWorker({
type: workerRequestMessageTypes.INIT,
databaseModuleFilePath: `${origin}${DATABASE_MODULE_FILE_PATH}`,
encryptionKey,
commQueryExecutorFilename,
});
console.info('Database initialization success');
return { type: 'init_success', worker, workerProxy };
} catch (error) {
console.error(`Database initialization failure`, error);
return { type: 'init_error' };
}
})(),
};
this.state = await this.state.initPromise;
}
async clearSensitiveData(): Promise<void> {
if (this.state.type === 'init_success') {
await this.state.workerProxy.scheduleOnWorker({
type: workerRequestMessageTypes.CLEAR_SENSITIVE_DATA,
});
this.state = { type: 'not_supported' };
}
}
async isDatabaseSupported(): Promise<boolean> {
if (this.state.type === 'init_in_progress') {
this.state = await this.state.initPromise;
}
return this.state.type === 'init_success';
}
async schedule(
payload: WorkerRequestMessage,
): Promise<?WorkerResponseMessage> {
if (this.state.type === 'init_in_progress') {
this.state = await this.state.initPromise;
}
if (this.state.type === 'not_supported') {
throw new Error('Database not supported');
}
if (this.state.type === 'init_error') {
throw new Error('Database could not be initialized');
}
return this.state.workerProxy.scheduleOnWorker(payload);
}
}
async function getSafariEncryptionKey(): Promise<SubtleCrypto$JsonWebKey> {
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<DatabaseModule> {
if (!databaseModule) {
databaseModule = new DatabaseModule();
const currentLoggedInUserID = preloadedState.currentUserInfo?.anonymous
? undefined
: preloadedState.currentUserInfo?.id;
await databaseModule.init(currentLoggedInUserID);
}
return databaseModule;
}
export { getDatabaseModule };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment