From d48410d79bf28240021b27254793b7350b0b322c Mon Sep 17 00:00:00 2001 From: cnoelle <19640096+cnoelle@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:18:30 +0200 Subject: [PATCH] update db version if required tables do not exist yet --- package.json | 2 +- src/impl/CacheImpl.ts | 53 ++++++++++++++++++++++++++++++------- test/testCacheWithMemory.js | 2 +- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c4ae100..db099a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lru-cache-idb", - "version": "0.2.0", + "version": "0.3.0", "description": "A least-recently-used (LRU) cache for web applications based on IndexedDB.", "type": "module", "types": "./dist/cache.d.ts", diff --git a/src/impl/CacheImpl.ts b/src/impl/CacheImpl.ts index 7ea88db..e4c4eac 100644 --- a/src/impl/CacheImpl.ts +++ b/src/impl/CacheImpl.ts @@ -18,7 +18,6 @@ export class LruCacheIndexedDBImpl implements LruCacheIndexedDB { readonly #indexedDB: IDBFactory; readonly #IDBKeyRange: any; readonly #database: string; - readonly #dbVersion: number = 1; readonly #itemsStorage: string; readonly #accessTimesStorage: string; readonly #config: LruIdbConfig; @@ -26,6 +25,7 @@ export class LruCacheIndexedDBImpl implements LruCacheIndexedDB { readonly #accessTimes: Table<{t: MillisecondsSinceEpoch}>; readonly #persistenceOrchestrator: PersistenceOrchestrator|undefined; readonly #dbLoader: (options?: CacheRequestOptions) => Promise; + #dbInitialized: boolean = false; readonly #eviction: PeriodicTask|undefined; // TODO interruptible? @@ -342,15 +342,50 @@ export class LruCacheIndexedDBImpl implements LruCacheIndexedDB { keysSorted.forEach(key => this.#memory!.delete(key)); } - readonly #openDb = (options?: CacheRequestOptions): Promise => { + // must only be called from onupgradeneeded callback + #createObjectStores(db: IDBDatabase) { + const objectStores = db.objectStoreNames; + const hasItems = objectStores.contains(this.#itemsStorage); + const hasAccessTimes = objectStores.contains(this.#accessTimesStorage); + if (!hasItems) + db.createObjectStore(this.#itemsStorage); + if (!hasAccessTimes) { + const timesStore = db.createObjectStore(this.#accessTimesStorage); + timesStore.createIndex("time", "t", {unique: false}); + } + this.#dbInitialized = true; + } + + #initializeDb = async () => { + let nextDbVersion: number|undefined = undefined; + while (!this.#dbInitialized) { + const initPromise: Promise<[number, boolean]> = new Promise<[number, boolean]>((resolve, reject) => { + const request: IDBOpenDBRequest = this.#indexedDB.open(this.#database, nextDbVersion); // open without explicit version initially, to get the latest + request.onupgradeneeded = (event) => this.#createObjectStores((event.target as any).result); + request.onerror = reject; + request.onsuccess = event => { + const db = (event.target as any).result as IDBDatabase; + if (!this.#dbInitialized) { + const objectStores = db.objectStoreNames; + const hasItems = objectStores.contains(this.#itemsStorage); + const hasAccessTimes = objectStores.contains(this.#accessTimesStorage); + if (hasItems && hasAccessTimes) + this.#dbInitialized = true; + } + resolve([db.version, this.#dbInitialized]); + }; + }); + const [dbVersion, initialized] = await initPromise; + nextDbVersion = dbVersion + 1; + } + } + + + readonly #openDb = async (options?: CacheRequestOptions): Promise => { + if (!this.#dbInitialized) + await this.#initializeDb(); const dbPromise: Promise = new Promise((resolve, reject) => { - const request: IDBOpenDBRequest = this.#indexedDB.open(this.#database, this.#dbVersion); - request.onupgradeneeded = (event) => { - const db: IDBDatabase = (event.target as any).result; - const objectStore = db.createObjectStore(this.#itemsStorage /*, { keyPath: "key" }*/); - const timesStore = db.createObjectStore(this.#accessTimesStorage); - timesStore.createIndex("time", "t", {unique: false}); - }; + const request: IDBOpenDBRequest = this.#indexedDB.open(this.#database /*, this.#dbVersion*/); // open without explicit version, to get the latest request.onerror = reject; options?.signal?.addEventListener("abort", reject, {once: true}); request.onsuccess = event => { diff --git a/test/testCacheWithMemory.js b/test/testCacheWithMemory.js index b08aa8e..5ef2c0f 100644 --- a/test/testCacheWithMemory.js +++ b/test/testCacheWithMemory.js @@ -361,7 +361,7 @@ test("peek does not affect access times for default cache and immediate persiste await defaultCache.close(); }); -test.only("Order of items works with in-memory updates in memory cache", async t => { +test("Order of items works with in-memory updates in memory cache", async t => { const persistencePeriod = 50; const defaultCache = createFakeIdb({persistencePeriod: persistencePeriod, memoryConfig: { maxItemsInMemory: 10 }}); const obj1 = {a: "test1", b: 1};