summaryrefslogtreecommitdiff
path: root/ext/js/data/database.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/data/database.js')
-rw-r--r--ext/js/data/database.js206
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;