diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts new file mode 100644 index 000000000..c83ce8d1e --- /dev/null +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -0,0 +1,62 @@ +import {promisifyRequest} from 'idb-keyval'; +import type {UseStore} from 'idb-keyval'; +import {logInfo} from '../../../Logger'; + +// This is a copy of the createStore function from idb-keyval, we need a custom implementation +// because we need to create the database manually in order to ensure that the store exists before we use it. +// If the store does not exist, idb-keyval will throw an error +// source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12 +function createStore(dbName: string, storeName: string): UseStore { + let dbp: Promise | undefined; + const getDB = () => { + if (dbp) return dbp; + const request = indexedDB.open(dbName); + request.onupgradeneeded = () => request.result.createObjectStore(storeName); + dbp = promisifyRequest(request); + + dbp.then( + (db) => { + // It seems like Safari sometimes likes to just close the connection. + // It's supposed to fire this event when that happens. Let's hope it does! + // eslint-disable-next-line no-param-reassign + db.onclose = () => (dbp = undefined); + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ); + return dbp; + }; + + // Ensures the store exists in the DB. If missing, bumps the version to trigger + // onupgradeneeded, recreates the store, and returns a promise to the new DB. + const verifyStoreExists = (db: IDBDatabase) => { + if (db.objectStoreNames.contains(storeName)) { + return db; + } + + logInfo(`Store ${storeName} does not exist in database ${dbName}.`); + const nextVersion = db.version + 1; + db.close(); + + const request = indexedDB.open(dbName, nextVersion); + request.onupgradeneeded = () => { + const updatedDatabase = request.result; + if (updatedDatabase.objectStoreNames.contains(storeName)) { + return; + } + + logInfo(`Creating store ${storeName} in database ${dbName}.`); + updatedDatabase.createObjectStore(storeName); + }; + + dbp = promisifyRequest(request); + return dbp; + }; + + return (txMode, callback) => + getDB() + .then(verifyStoreExists) + .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); +} + +export default createStore; diff --git a/lib/storage/providers/IDBKeyValProvider.ts b/lib/storage/providers/IDBKeyValProvider/index.ts similarity index 87% rename from lib/storage/providers/IDBKeyValProvider.ts rename to lib/storage/providers/IDBKeyValProvider/index.ts index 0fd2a63ec..929c2e65c 100644 --- a/lib/storage/providers/IDBKeyValProvider.ts +++ b/lib/storage/providers/IDBKeyValProvider/index.ts @@ -1,12 +1,15 @@ import type {UseStore} from 'idb-keyval'; -import {set, keys, getMany, setMany, get, clear, del, delMany, createStore, promisifyRequest} from 'idb-keyval'; -import utils from '../../utils'; -import type StorageProvider from './types'; -import type {OnyxKey, OnyxValue} from '../../types'; +import {set, keys, getMany, setMany, get, clear, del, delMany, promisifyRequest} from 'idb-keyval'; +import utils from '../../../utils'; +import type StorageProvider from '../types'; +import type {OnyxKey, OnyxValue} from '../../../types'; +import createStore from './createStore'; // We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB // which might not be available in certain environments that load the bundle (e.g. electron main process). let idbKeyValStore: UseStore; +const DB_NAME = 'OnyxDB'; +const STORE_NAME = 'keyvaluepairs'; const provider: StorageProvider = { /** @@ -17,12 +20,15 @@ const provider: StorageProvider = { * Initializes the storage provider */ init() { - const newIdbKeyValStore = createStore('OnyxDB', 'keyvaluepairs'); + const newIdbKeyValStore = createStore(DB_NAME, STORE_NAME); - if (newIdbKeyValStore == null) throw Error('IDBKeyVal store could not be created'); + if (newIdbKeyValStore == null) { + throw Error('IDBKeyVal store could not be created'); + } idbKeyValStore = newIdbKeyValStore; }, + setItem: (key, value) => { if (value === null) { provider.removeItem(key);