diff --git a/src/common/KeyValueDB.ts b/src/common/KeyValueDB.ts index ce9a229..3b02e07 100644 --- a/src/common/KeyValueDB.ts +++ b/src/common/KeyValueDB.ts @@ -3,7 +3,9 @@ import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts import { serialized } from "octagonal-wheels/concurrency/lock"; import { Logger } from "octagonal-wheels/common/logger"; const databaseCache: { [key: string]: IDBPDatabase } = {}; -export const OpenKeyValueDatabase = async (dbKey: string): Promise => { +export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts"; + +export const _OpenKeyValueDatabase = async (dbKey: string): Promise => { if (dbKey in databaseCache) { databaseCache[dbKey].close(); delete databaseCache[dbKey]; @@ -82,7 +84,7 @@ export const OpenKeyValueDatabase = async (dbKey: string): Promise(); + +export async function OpenKeyValueDatabase(dbKey: string): Promise { + return await serialized(`OpenKeyValueDatabase-${dbKey}`, async () => { + const cachedDB = databaseCache.get(dbKey); + if (cachedDB) { + if (!cachedDB.isDestroyed) { + return cachedDB; + } + await cachedDB.ensuredDestroyed; + databaseCache.delete(dbKey); + } + const newDB = new IDBKeyValueDatabase(dbKey); + try { + await newDB.getIsReady(); + databaseCache.set(dbKey, newDB); + return newDB; + } catch (e) { + databaseCache.delete(dbKey); + throw e; + } + }); +} + +export class IDBKeyValueDatabase implements KeyValueDatabase { + protected _dbPromise: Promise> | null = null; + protected dbKey: string; + protected storeKey: string; + protected _isDestroyed: boolean = false; + protected destroyedPromise: Promise | null = null; + + get isDestroyed() { + return this._isDestroyed; + } + get ensuredDestroyed(): Promise { + if (this.destroyedPromise) { + return this.destroyedPromise; + } + return Promise.resolve(); + } + + async getIsReady(): Promise { + await this.ensureDB(); + return this.isDestroyed === false; + } + + protected ensureDB() { + if (this._isDestroyed) { + throw new Error("Database is destroyed"); + } + if (this._dbPromise) { + return this._dbPromise; + } + this._dbPromise = openDB(this.dbKey, undefined, { + upgrade: (db, _oldVersion, _newVersion, _transaction, _event) => { + if (!db.objectStoreNames.contains(this.storeKey)) { + return db.createObjectStore(this.storeKey); + } + }, + blocking: (currentVersion, blockedVersion, event) => { + Logger( + `Blocking database open for ${this.dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`, + LOG_LEVEL_VERBOSE + ); + // This `this` is not this openDB instance, previously opened DB. Let it be closed in the terminated handler. + void this.closeDB(true); + }, + blocked: (currentVersion, blockedVersion, event) => { + Logger( + `Database open blocked for ${this.dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`, + LOG_LEVEL_VERBOSE + ); + }, + terminated: () => { + Logger(`Database connection terminated for ${this.dbKey}`, LOG_LEVEL_VERBOSE); + this._dbPromise = null; + }, + }).catch((e) => { + this._dbPromise = null; + throw e; + }); + return this._dbPromise; + } + protected async closeDB(setDestroyed: boolean = false) { + if (this._dbPromise) { + const tempPromise = this._dbPromise; + this._dbPromise = null; + try { + const dbR = await tempPromise; + dbR.close(); + } catch (e) { + Logger(`Error closing database`); + Logger(e, LOG_LEVEL_VERBOSE); + } + } + this._dbPromise = null; + if (setDestroyed) { + this._isDestroyed = true; + this.destroyedPromise = Promise.resolve(); + } + } + get DB(): Promise> { + if (this._isDestroyed) { + return Promise.reject(new Error("Database is destroyed")); + } + return this.ensureDB(); + } + + constructor(dbKey: string) { + this.dbKey = dbKey; + this.storeKey = dbKey; + } + async get(key: IDBValidKey): Promise { + const db = await this.DB; + return await db.get(this.storeKey, key); + } + async set(key: IDBValidKey, value: U): Promise { + const db = await this.DB; + await db.put(this.storeKey, value, key); + return key; + } + async del(key: IDBValidKey): Promise { + const db = await this.DB; + return await db.delete(this.storeKey, key); + } + async clear(): Promise { + const db = await this.DB; + return await db.clear(this.storeKey); + } + async keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise { + const db = await this.DB; + return await db.getAllKeys(this.storeKey, query, count); + } + async close(): Promise { + await this.closeDB(); + } + async destroy(): Promise { + this._isDestroyed = true; + this.destroyedPromise = (async () => { + await this.closeDB(); + await deleteDB(this.dbKey, { + blocked: () => { + Logger(`Database delete blocked for ${this.dbKey}`); + }, + }); + })(); + await this.destroyedPromise; + } +} diff --git a/src/lib b/src/lib index bc761fc..8fbd310 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit bc761fcf579e3a9dee89d7679a56fa4bd56af176 +Subproject commit 8fbd310e7f680c4896ee47a6e4a442182c7d0fec diff --git a/src/modules/essential/ModuleKeyValueDB.ts b/src/modules/essential/ModuleKeyValueDB.ts index 3226203..0194649 100644 --- a/src/modules/essential/ModuleKeyValueDB.ts +++ b/src/modules/essential/ModuleKeyValueDB.ts @@ -6,9 +6,9 @@ import { AbstractModule } from "../AbstractModule.ts"; import type { LiveSyncCore } from "../../main.ts"; export class ModuleKeyValueDB extends AbstractModule { - tryCloseKvDB() { + async tryCloseKvDB() { try { - this.core.kvDB?.close(); + await this.core.kvDB?.close(); return true; } catch (e) { this._log("Failed to close KeyValueDB", LOG_LEVEL_VERBOSE); @@ -19,7 +19,7 @@ export class ModuleKeyValueDB extends AbstractModule { async openKeyValueDB(): Promise { await delay(10); try { - this.tryCloseKvDB(); + await this.tryCloseKvDB(); await delay(10); await yieldMicrotask(); this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv"); @@ -33,12 +33,12 @@ export class ModuleKeyValueDB extends AbstractModule { } return true; } - _onDBUnload(db: LiveSyncLocalDB) { - if (this.core.kvDB) this.core.kvDB.close(); + async _onDBUnload(db: LiveSyncLocalDB) { + if (this.core.kvDB) await this.core.kvDB.close(); return Promise.resolve(true); } - _onDBClose(db: LiveSyncLocalDB) { - if (this.core.kvDB) this.core.kvDB.close(); + async _onDBClose(db: LiveSyncLocalDB) { + if (this.core.kvDB) await this.core.kvDB.close(); return Promise.resolve(true); }