From 2215bfc80f665ef99786ca371445bcd3cd534ce8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 29 Sep 2025 18:34:26 +0200 Subject: [PATCH 1/3] Add IDBKeyValProvider with createStore function for manual store creation --- .../IDBKeyValProvider/createStore.ts | 60 +++++++++++++++++++ .../index.ts} | 19 ++++-- 2 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 lib/storage/providers/IDBKeyValProvider/createStore.ts rename lib/storage/providers/{IDBKeyValProvider.ts => IDBKeyValProvider/index.ts} (87%) diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts new file mode 100644 index 000000000..d375afc5c --- /dev/null +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -0,0 +1,60 @@ +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, but we need to use it to create the store manually. +// We do this 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 database: Promise | undefined; + const getDB = () => { + if (database) return database; + const request = indexedDB.open(dbName); + request.onupgradeneeded = () => request.result.createObjectStore(storeName); + database = promisifyRequest(request); + database.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 = () => (database = undefined); + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ); + + return database; + }; + + const fixStore = (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); + }; + + database = promisifyRequest(request); + return database; + }; + + return (txMode, callback) => + getDB() + .then(fixStore) + .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..01a7a33bd 100644 --- a/lib/storage/providers/IDBKeyValProvider.ts +++ b/lib/storage/providers/IDBKeyValProvider/index.ts @@ -1,12 +1,16 @@ +/* eslint-disable no-console */ 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 +21,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); From 313a6f56fe69df27bf03df8fe3c91f1008227989 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Oct 2025 12:14:05 +0200 Subject: [PATCH 2/3] Remove unused eslint-disable comment --- lib/storage/providers/IDBKeyValProvider/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/storage/providers/IDBKeyValProvider/index.ts b/lib/storage/providers/IDBKeyValProvider/index.ts index 01a7a33bd..929c2e65c 100644 --- a/lib/storage/providers/IDBKeyValProvider/index.ts +++ b/lib/storage/providers/IDBKeyValProvider/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type {UseStore} from 'idb-keyval'; import {set, keys, getMany, setMany, get, clear, del, delMany, promisifyRequest} from 'idb-keyval'; import utils from '../../../utils'; From 0680f6cc8951e67540bc3c0bd42eda723cc9d141 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 2 Oct 2025 14:08:08 +0200 Subject: [PATCH 3/3] Refactor createStore function in IDBKeyValProvider --- .../IDBKeyValProvider/createStore.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts index d375afc5c..c83ce8d1e 100644 --- a/lib/storage/providers/IDBKeyValProvider/createStore.ts +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -2,32 +2,34 @@ 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, but we need to use it to create the store manually. -// We do this because we need to create the database manually in order to ensure that the store exists before we use it. +// 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 database: Promise | undefined; + let dbp: Promise | undefined; const getDB = () => { - if (database) return database; + if (dbp) return dbp; const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); - database = promisifyRequest(request); - database.then( + 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 = () => (database = undefined); + db.onclose = () => (dbp = undefined); }, // eslint-disable-next-line @typescript-eslint/no-empty-function () => {}, ); - - return database; + return dbp; }; - const fixStore = (db: IDBDatabase) => { + // 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; } @@ -47,13 +49,13 @@ function createStore(dbName: string, storeName: string): UseStore { updatedDatabase.createObjectStore(storeName); }; - database = promisifyRequest(request); - return database; + dbp = promisifyRequest(request); + return dbp; }; return (txMode, callback) => getDB() - .then(fixStore) + .then(verifyStoreExists) .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); }