diff --git a/package.json b/package.json index 7f1c23f..f634515 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.25.23", + "version": "0.25.24", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/database.ts b/src/database.ts index 9734690..c7ff4fc 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,5 +1,6 @@ import {findByProp} from './array.ts'; import {ASet} from './aset.ts'; +import {sleepWhile} from './time.ts'; export type TableOptions = { name: string; @@ -7,11 +8,24 @@ export type TableOptions = { autoIncrement?: boolean; }; +class AsyncLock { + private p = Promise.resolve(); + run(fn: () => Promise): Promise { + const res = this.p.then(fn, fn); + this.p = res.then(() => {}, () => {}); + return res; +} + } + export class Database { + private schemaLock = new AsyncLock(); + private upgrading = false; + connection!: Promise; - ready = false; tables!: TableOptions[]; + get ready() { return !this.upgrading; } + constructor(public readonly database: string, tables?: (string | TableOptions)[], public version?: number) { this.connection = new Promise((resolve, reject) => { const req = indexedDB.open(this.database, this.version); @@ -26,7 +40,7 @@ export class Database { const db = req.result; const existing = Array.from(db.objectStoreNames); if(!tables) this.tables = existing.map(t => { - const tx = db.transaction(t, 'readonly', ) + const tx = db.transaction(t, 'readonly'); const store = tx.objectStore(t); return {name: t, key: store.keyPath}; }); @@ -39,10 +53,11 @@ export class Database { this.version = db.version; resolve(db); } - this.ready = true; + this.upgrading = false; }; req.onupgradeneeded = () => { + this.upgrading = true; const db = req.result; const existingTables = new ASet(Array.from(db.objectStoreNames)); if(tables) { @@ -52,7 +67,7 @@ export class Database { const t = this.tables.find(findByProp('name', name)); db.createObjectStore(name, { keyPath: t?.key, - autoIncrement: t?.autoIncrement || !t?.key + autoIncrement: t?.autoIncrement || !t?.key, }); }); } @@ -60,22 +75,30 @@ export class Database { }); } + waitForUpgrade = () => sleepWhile(() => this.upgrading); + async createTable(table: string | TableOptions): Promise> { - if(typeof table == 'string') table = {name: table}; - const conn = await this.connection; - if(!this.includes(table.name)) { - conn.close(); - Object.assign(this, new Database(this.database, [...this.tables, table], (this.version ?? 0) + 1)); - } - return this.table(table.name); + return this.schemaLock.run(async () => { + if(typeof table == 'string') table = { name: table }; + const conn = await this.connection; + if(!this.includes(table.name)) { + conn.close(); + Object.assign(this, new Database(this.database, [...this.tables, table], (this.version ?? 0) + 1)); + await this.connection; + } + return this.table(table.name); + }); } async deleteTable(table: string | TableOptions): Promise { - if(typeof table == 'string') table = {name: table}; - if(!this.includes(table.name)) return; - const conn = await this.connection; - conn.close(); - Object.assign(this, new Database(this.database, this.tables.filter(t => t.name != table.name), (this.version ?? 0) + 1)); + return this.schemaLock.run(async () => { + if(typeof table == 'string') table = { name: table }; + if(!this.includes(table.name)) return; + const conn = await this.connection; + conn.close(); + Object.assign(this, new Database(this.database, this.tables.filter(t => t.name != (table).name), (this.version ?? 0) + 1)); + await this.connection; + }); } includes(name: any): boolean { @@ -96,12 +119,13 @@ export class Table { } async tx(table: string, fn: (store: IDBObjectStore) => IDBRequest, readonly = false): Promise { + await this.database.waitForUpgrade(); const db = await this.database.connection; const tx = db.transaction(table, readonly ? 'readonly' : 'readwrite'); const store = tx.objectStore(table); return new Promise((resolve, reject) => { const request = fn(store); - request.onsuccess = () => resolve(request.result as R); // ✅ explicit cast + request.onsuccess = () => resolve(request.result as R); request.onerror = () => reject(request.error); }); }