diff options
Diffstat (limited to 'ext/js/data/database.js')
-rw-r--r-- | ext/js/data/database.js | 206 |
1 files changed, 187 insertions, 19 deletions
diff --git a/ext/js/data/database.js b/ext/js/data/database.js index 8e818d8b..026945ca 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -16,12 +16,22 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +/** + * @template {string} TObjectStoreName + */ export class Database { constructor() { + /** @type {?IDBDatabase} */ this._db = null; + /** @type {boolean} */ this._isOpening = false; } + /** + * @param {string} databaseName + * @param {number} version + * @param {import('database').StructureDefinition<TObjectStoreName>[]} structure + */ async open(databaseName, version, structure) { if (this._db !== null) { throw new Error('Database already open'); @@ -40,6 +50,9 @@ export class Database { } } + /** + * @throws {Error} + */ close() { if (this._db === null) { throw new Error('Database is not open'); @@ -49,14 +62,26 @@ export class Database { this._db = null; } + /** + * @returns {boolean} + */ isOpening() { return this._isOpening; } + /** + * @returns {boolean} + */ isOpen() { return this._db !== null; } + /** + * @param {string[]} storeNames + * @param {IDBTransactionMode} mode + * @returns {IDBTransaction} + * @throws {Error} + */ transaction(storeNames, mode) { if (this._db === null) { throw new Error(this._isOpening ? 'Database not ready' : 'Database not open'); @@ -64,6 +89,13 @@ export class Database { return this._db.transaction(storeNames, mode); } + /** + * @param {TObjectStoreName} objectStoreName + * @param {unknown[]} items + * @param {number} start + * @param {number} count + * @returns {Promise<void>} + */ bulkAdd(objectStoreName, items, start, count) { return new Promise((resolve, reject) => { if (start + count > items.length) { @@ -84,6 +116,15 @@ export class Database { }); } + /** + * @template [TData=unknown] + * @template [TResult=unknown] + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {?IDBValidKey|IDBKeyRange} query + * @param {(results: TResult[], data: TData) => void} onSuccess + * @param {(reason: unknown, data: TData) => void} onError + * @param {TData} data + */ getAll(objectStoreOrIndex, query, onSuccess, onError, data) { if (typeof objectStoreOrIndex.getAll === 'function') { this._getAllFast(objectStoreOrIndex, query, onSuccess, onError, data); @@ -92,6 +133,12 @@ export class Database { } } + /** + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {IDBValidKey|IDBKeyRange} query + * @param {(value: IDBValidKey[]) => void} onSuccess + * @param {(reason?: unknown) => void} onError + */ getAllKeys(objectStoreOrIndex, query, onSuccess, onError) { if (typeof objectStoreOrIndex.getAllKeys === 'function') { this._getAllKeysFast(objectStoreOrIndex, query, onSuccess, onError); @@ -100,6 +147,18 @@ export class Database { } } + /** + * @template TPredicateArg + * @template [TResult=unknown] + * @template [TResultDefault=unknown] + * @param {TObjectStoreName} objectStoreName + * @param {?string} indexName + * @param {?IDBValidKey|IDBKeyRange} query + * @param {?((value: TResult|TResultDefault, predicateArg: TPredicateArg) => boolean)} predicate + * @param {TPredicateArg} predicateArg + * @param {TResultDefault} defaultValue + * @returns {Promise<TResult|TResultDefault>} + */ find(objectStoreName, indexName, query, predicate, predicateArg, defaultValue) { return new Promise((resolve, reject) => { const transaction = this.transaction([objectStoreName], 'readonly'); @@ -109,12 +168,26 @@ export class Database { }); } + /** + * @template TData + * @template TPredicateArg + * @template [TResult=unknown] + * @template [TResultDefault=unknown] + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {?IDBValidKey|IDBKeyRange} query + * @param {(value: TResult|TResultDefault, data: TData) => void} resolve + * @param {(reason: unknown, data: TData) => void} reject + * @param {TData} data + * @param {?((value: TResult, predicateArg: TPredicateArg) => boolean)} predicate + * @param {TPredicateArg} predicateArg + * @param {TResultDefault} defaultValue + */ findFirst(objectStoreOrIndex, query, resolve, reject, data, predicate, predicateArg, defaultValue) { const noPredicate = (typeof predicate !== 'function'); const request = objectStoreOrIndex.openCursor(query, 'next'); - request.onerror = (e) => reject(e.target.error, data); + request.onerror = (e) => reject(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data); request.onsuccess = (e) => { - const cursor = e.target.result; + const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result; if (cursor) { const {value} = cursor; if (noPredicate || predicate(value, predicateArg)) { @@ -128,19 +201,33 @@ export class Database { }; } + /** + * @param {import('database').CountTarget[]} targets + * @param {(results: number[]) => void} resolve + * @param {(reason?: unknown) => void} reject + */ bulkCount(targets, resolve, reject) { const targetCount = targets.length; if (targetCount <= 0) { - resolve(); + resolve([]); return; } let completedCount = 0; + /** @type {number[]} */ const results = new Array(targetCount).fill(null); - const onError = (e) => reject(e.target.error); + /** + * @param {Event} e + * @returns {void} + */ + const onError = (e) => reject(/** @type {IDBRequest<number>} */ (e.target).error); + /** + * @param {Event} e + * @param {number} index + */ const onSuccess = (e, index) => { - const count = e.target.result; + const count = /** @type {IDBRequest<number>} */ (e.target).result; results[index] = count; if (++completedCount >= targetCount) { resolve(results); @@ -156,6 +243,11 @@ export class Database { } } + /** + * @param {TObjectStoreName} objectStoreName + * @param {IDBValidKey|IDBKeyRange} key + * @returns {Promise<void>} + */ delete(objectStoreName, key) { return new Promise((resolve, reject) => { const transaction = this._readWriteTransaction([objectStoreName], resolve, reject); @@ -165,12 +257,23 @@ export class Database { }); } + /** + * @param {TObjectStoreName} objectStoreName + * @param {?string} indexName + * @param {IDBKeyRange} query + * @param {?(keys: IDBValidKey[]) => IDBValidKey[]} filterKeys + * @param {?(completedCount: number, totalCount: number) => void} onProgress + * @returns {Promise<void>} + */ bulkDelete(objectStoreName, indexName, query, filterKeys=null, onProgress=null) { return new Promise((resolve, reject) => { const transaction = this._readWriteTransaction([objectStoreName], resolve, reject); const objectStore = transaction.objectStore(objectStoreName); const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore; + /** + * @param {IDBValidKey[]} keys + */ const onGetKeys = (keys) => { try { if (typeof filterKeys === 'function') { @@ -187,10 +290,14 @@ export class Database { }); } + /** + * @param {string} databaseName + * @returns {Promise<void>} + */ static deleteDatabase(databaseName) { return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(databaseName); - request.onerror = (e) => reject(e.target.error); + request.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error); request.onsuccess = () => resolve(); request.onblocked = () => reject(new Error('Database deletion blocked')); }); @@ -198,24 +305,37 @@ export class Database { // Private + /** + * @param {string} name + * @param {number} version + * @param {import('database').UpdateFunction} onUpgradeNeeded + * @returns {Promise<IDBDatabase>} + */ _open(name, version, onUpgradeNeeded) { return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onupgradeneeded = (event) => { try { - request.transaction.onerror = (e) => reject(e.target.error); - onUpgradeNeeded(request.result, request.transaction, event.oldVersion, event.newVersion); + const transaction = /** @type {IDBTransaction} */ (request.transaction); + transaction.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error); + onUpgradeNeeded(request.result, transaction, event.oldVersion, event.newVersion); } catch (e) { reject(e); } }; - request.onerror = (e) => reject(e.target.error); + request.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error); request.onsuccess = () => resolve(request.result); }); } + /** + * @param {IDBDatabase} db + * @param {IDBTransaction} transaction + * @param {number} oldVersion + * @param {import('database').StructureDefinition<TObjectStoreName>[]} upgrades + */ _upgrade(db, transaction, oldVersion, upgrades) { for (const {version, stores} of upgrades) { if (oldVersion >= version) { continue; } @@ -238,6 +358,11 @@ export class Database { } } + /** + * @param {DOMStringList} list + * @param {string} value + * @returns {boolean} + */ _listContains(list, value) { for (let i = 0, ii = list.length; i < ii; ++i) { if (list[i] === value) { return true; } @@ -245,18 +370,37 @@ export class Database { return false; } + /** + * @template [TData=unknown] + * @template [TResult=unknown] + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {?IDBValidKey|IDBKeyRange} query + * @param {(results: TResult[], data: TData) => void} onSuccess + * @param {(reason: unknown, data: TData) => void} onReject + * @param {TData} data + */ _getAllFast(objectStoreOrIndex, query, onSuccess, onReject, data) { const request = objectStoreOrIndex.getAll(query); - request.onerror = (e) => onReject(e.target.error, data); - request.onsuccess = (e) => onSuccess(e.target.result, data); + request.onerror = (e) => onReject(/** @type {IDBRequest<import('core').SafeAny[]>} */ (e.target).error, data); + request.onsuccess = (e) => onSuccess(/** @type {IDBRequest<import('core').SafeAny[]>} */ (e.target).result, data); } + /** + * @template [TData=unknown] + * @template [TResult=unknown] + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {?IDBValidKey|IDBKeyRange} query + * @param {(results: TResult[], data: TData) => void} onSuccess + * @param {(reason: unknown, data: TData) => void} onReject + * @param {TData} data + */ _getAllUsingCursor(objectStoreOrIndex, query, onSuccess, onReject, data) { + /** @type {TResult[]} */ const results = []; const request = objectStoreOrIndex.openCursor(query, 'next'); - request.onerror = (e) => onReject(e.target.error, data); + request.onerror = (e) => onReject(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data); request.onsuccess = (e) => { - const cursor = e.target.result; + const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result; if (cursor) { results.push(cursor.value); cursor.continue(); @@ -266,18 +410,31 @@ export class Database { }; } + /** + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {IDBValidKey|IDBKeyRange} query + * @param {(value: IDBValidKey[]) => void} onSuccess + * @param {(reason?: unknown) => void} onError + */ _getAllKeysFast(objectStoreOrIndex, query, onSuccess, onError) { const request = objectStoreOrIndex.getAllKeys(query); - request.onerror = (e) => onError(e.target.error); - request.onsuccess = (e) => onSuccess(e.target.result); + request.onerror = (e) => onError(/** @type {IDBRequest<IDBValidKey[]>} */ (e.target).error); + request.onsuccess = (e) => onSuccess(/** @type {IDBRequest<IDBValidKey[]>} */ (e.target).result); } + /** + * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex + * @param {IDBValidKey|IDBKeyRange} query + * @param {(value: IDBValidKey[]) => void} onSuccess + * @param {(reason?: unknown) => void} onError + */ _getAllKeysUsingCursor(objectStoreOrIndex, query, onSuccess, onError) { + /** @type {IDBValidKey[]} */ const results = []; const request = objectStoreOrIndex.openKeyCursor(query, 'next'); - request.onerror = (e) => onError(e.target.error); + request.onerror = (e) => onError(/** @type {IDBRequest<?IDBCursor>} */ (e.target).error); request.onsuccess = (e) => { - const cursor = e.target.result; + const cursor = /** @type {IDBRequest<?IDBCursor>} */ (e.target).result; if (cursor) { results.push(cursor.primaryKey); cursor.continue(); @@ -287,6 +444,11 @@ export class Database { }; } + /** + * @param {IDBObjectStore} objectStore + * @param {IDBValidKey[]} keys + * @param {?(completedCount: number, totalCount: number) => void} onProgress + */ _bulkDeleteInternal(objectStore, keys, onProgress) { const count = keys.length; if (count === 0) { return; } @@ -295,7 +457,7 @@ export class Database { const onSuccess = () => { ++completedCount; try { - onProgress(completedCount, count); + /** @type {(completedCount: number, totalCount: number) => void}} */ (onProgress)(completedCount, count); } catch (e) { // NOP } @@ -310,9 +472,15 @@ export class Database { } } + /** + * @param {string[]} storeNames + * @param {() => void} resolve + * @param {(reason?: unknown) => void} reject + * @returns {IDBTransaction} + */ _readWriteTransaction(storeNames, resolve, reject) { const transaction = this.transaction(storeNames, 'readwrite'); - transaction.onerror = (e) => reject(e.target.error); + transaction.onerror = (e) => reject(/** @type {IDBTransaction} */ (e.target).error); transaction.onabort = () => reject(new Error('Transaction aborted')); transaction.oncomplete = () => resolve(); return transaction; |