Something went wrong. Try again.
Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619/* * Copyright (C) 2023-2025 Yomitan Authors * Copyright (C) 2020-2022 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */
import {toError} from '../core/to-error.js';
/** * @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'); } if (this._isOpening) { throw new Error('Already opening'); }
try { this._isOpening = true; this._db = await this._open(databaseName, version, (db, transaction, oldVersion) => { if (structure !== null) { this._upgrade(db, transaction, oldVersion, structure); } }); if (this._db.objectStoreNames.length === 0) { this.close(); await Database.deleteDatabase(databaseName); this._isOpening = false; await this.open(databaseName, version, structure); } } finally { this._isOpening = false; } }
/** * @throws {Error} */ close() { if (this._db === null) { throw new Error('Database is not open'); }
this._db.close(); this._db = null; }
/** * Returns true if the database opening is in process. * @returns {boolean} */ isOpening() { return this._isOpening; }
/** * Returns true if the database is fully opened. * @returns {boolean} */ isOpen() { return this._db !== null; }
/** * Returns a new transaction with the given mode ("readonly" or "readwrite") and scope which can be a single object store name or an array of names. * @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'); } try { return this._db.transaction(storeNames, mode); } catch (e) { throw new Error(toError(e).message + '\nDatabase transaction error, you may need to Delete All dictionaries to reset the database or manually delete the Indexed DB database.'); } }
/** * Add items in bulk to the object store. * _count_ items will be added, starting from _start_ index of _items_ list. * @param {TObjectStoreName} objectStoreName * @param {unknown[]} items List of items to add. * @param {number} start Start index. Added items begin at _items_[_start_]. * @param {number} count Count of items to add. * @returns {Promise<void>} */ bulkAdd(objectStoreName, items, start, count) { return new Promise((resolve, reject) => { if (start + count > items.length) { count = items.length - start; }
if (count <= 0) { resolve(); return; }
const transaction = this._readWriteTransaction([objectStoreName], resolve, reject); const objectStore = transaction.objectStore(objectStoreName); for (let i = start, ii = start + count; i < ii; ++i) { objectStore.add(items[i]); } transaction.commit(); }); }
/** * Add a single item and return a promise containing the resulting primaryKey. * Holding onto the result value makes the GC not clean up until much later even if the value is not used. * Only call this method if the primaryKey of the added value is required. * @param {TObjectStoreName} objectStoreName * @param {unknown} item Item to add. * @returns {Promise<IDBRequest<IDBValidKey>>} */ addWithResult(objectStoreName, item) { return new Promise((resolve, reject) => { const transaction = this._readWriteTransaction([objectStoreName], () => {}, reject); const objectStore = transaction.objectStore(objectStoreName); const result = objectStore.add(item); transaction.commit(); resolve(result); }); }
/** * Update items in bulk to the object store. * Items that do not exist will be added. * _count_ items will be updated, starting from _start_ index of _items_ list. * @param {TObjectStoreName} objectStoreName * @param {import('dictionary-database').DatabaseUpdateItem[]} items List of items to update. * @param {number} start Start index. Updated items begin at _items_[_start_]. * @param {number} count Count of items to update. * @returns {Promise<void>} */ bulkUpdate(objectStoreName, items, start, count) { return new Promise((resolve, reject) => { if (start + count > items.length) { count = items.length - start; }
if (count <= 0) { resolve(); return; }
const transaction = this._readWriteTransaction([objectStoreName], resolve, reject); const objectStore = transaction.objectStore(objectStoreName);
for (let i = start, ii = start + count; i < ii; ++i) { objectStore.put(items[i].data, items[i].primaryKey); } transaction.commit(); }); }
/** * @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); } else { this._getAllUsingCursor(objectStoreOrIndex, query, onSuccess, onError, data); } }
/** * @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); } else { this._getAllKeysUsingCursor(objectStoreOrIndex, query, onSuccess, onError); } }
/** * @template [TPredicateArg=unknown] * @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'); const objectStore = transaction.objectStore(objectStoreName); const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore; this.findFirst(objectStoreOrIndex, query, resolve, reject, null, predicate, predicateArg, defaultValue); }); }
/** * @template [TData=unknown] * @template [TPredicateArg=unknown] * @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(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data); request.onsuccess = (e) => { const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result; if (cursor) { /** @type {unknown} */ const value = cursor.value; if (noPredicate || predicate(/** @type {TResult} */ (value), predicateArg)) { resolve(/** @type {TResult} */ (value), data); } else { cursor.continue(); } } else { resolve(defaultValue, data); } }; }
/** * @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([]); return; }
let completedCount = 0; /** @type {number[]} */ const results = new Array(targetCount).fill(null);
/** * @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 = /** @type {IDBRequest<number>} */ (e.target).result; results[index] = count; if (++completedCount >= targetCount) { resolve(results); } };
for (let i = 0; i < targetCount; ++i) { const index = i; const [objectStoreOrIndex, query] = targets[i]; const request = objectStoreOrIndex.count(query); request.onerror = onError; request.onsuccess = (e) => onSuccess(e, index); } }
/** * Deletes records in store with the given key or in the given key range in query. * @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); const objectStore = transaction.objectStore(objectStoreName); objectStore.delete(key); transaction.commit(); }); }
/** * Delete items in bulk from the object store. * @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') { keys = filterKeys(keys); } this._bulkDeleteInternal(objectStore, keys, 1000, 0, onProgress, (error) => { if (error !== null) { transaction.commit(); } }); } catch (e) { reject(e); } };
this.getAllKeys(objectStoreOrIndex, query, onGetKeys, reject); }); }
/** * Attempts to delete the named database. * If the database already exists and there are open connections that don't close in response to a versionchange event, the request will be blocked until all they close. * If the request is successful request's result will be null. * @param {string} databaseName * @returns {Promise<void>} */ static deleteDatabase(databaseName) { return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(databaseName); request.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error); request.onsuccess = () => resolve(); request.onblocked = () => reject(new Error('Database deletion blocked')); }); }
// 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 { 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(/** @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; }
/** @type {[objectStoreName: string, value: import('database').StoreDefinition][]} */ const entries = Object.entries(stores); for (const [objectStoreName, {primaryKey, indices}] of entries) { const existingObjectStoreNames = transaction.objectStoreNames || db.objectStoreNames; const objectStore = ( this._listContains(existingObjectStoreNames, objectStoreName) ? transaction.objectStore(objectStoreName) : db.createObjectStore(objectStoreName, primaryKey) ); const existingIndexNames = objectStore.indexNames;
for (const indexName of indices) { if (this._listContains(existingIndexNames, indexName)) { continue; }
objectStore.createIndex(indexName, indexName, {}); } } } }
/** * @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; } } 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) => { const target = /** @type {IDBRequest<TResult[]>} */ (e.target); onReject(target.error, data); }; request.onsuccess = (e) => { const target = /** @type {IDBRequest<TResult[]>} */ (e.target); onSuccess(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(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data); request.onsuccess = (e) => { const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result; if (cursor) { /** @type {unknown} */ const value = cursor.value; results.push(/** @type {TResult} */ (value)); cursor.continue(); } else { onSuccess(results, data); } }; }
/** * @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(/** @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(/** @type {IDBRequest<?IDBCursor>} */ (e.target).error); request.onsuccess = (e) => { const cursor = /** @type {IDBRequest<?IDBCursor>} */ (e.target).result; if (cursor) { results.push(cursor.primaryKey); cursor.continue(); } else { onSuccess(results); } }; }
/** * @param {IDBObjectStore} objectStore The object store from which items are being deleted. * @param {IDBValidKey[]} keys An array of keys to delete from the object store. * @param {number} maxActiveRequests The maximum number of concurrent requests. * @param {number} maxActiveRequestsForContinue The maximum number of requests that can be active before the next set of requests is started. * For example: * - If this value is `0`, all of the `maxActiveRequests` requests must complete before another group of `maxActiveRequests` is started off. * - If the value is greater than or equal to `maxActiveRequests-1`, every time a single request completes, a new single request will be started. * @param {?(completedCount: number, totalCount: number) => void} onProgress An optional progress callback function. * @param {(error: ?Error) => void} onComplete A function which is called after all operations have finished. * If an error occured, the `error` parameter will be non-`null`. Otherwise, it will be `null`. * @throws {Error} An error is thrown if the input parameters are invalid. */ _bulkDeleteInternal(objectStore, keys, maxActiveRequests, maxActiveRequestsForContinue, onProgress, onComplete) { if (maxActiveRequests <= 0) { throw new Error(`maxActiveRequests has an invalid value: ${maxActiveRequests}`); } if (maxActiveRequestsForContinue < 0) { throw new Error(`maxActiveRequestsForContinue has an invalid value: ${maxActiveRequestsForContinue}`); }
const count = keys.length; if (count === 0) { onComplete(null); return; }
let completedCount = 0; let completed = false; let index = 0; let active = 0;
const onSuccess = () => { if (completed) { return; } --active; ++completedCount; if (onProgress !== null) { try { onProgress(completedCount, count); } catch (e) { // NOP } } if (completedCount >= count) { completed = true; onComplete(null); } else if (active <= maxActiveRequestsForContinue) { next(); } };
/** * @param {Event} event */ const onError = (event) => { if (completed) { return; } completed = true; const request = /** @type {IDBRequest<undefined>} */ (event.target); const {error} = request; onComplete(error); };
const next = () => { for (; index < count && active < maxActiveRequests; ++index) { const key = keys[index]; const request = objectStore.delete(key); request.onsuccess = onSuccess; request.onerror = onError; ++active; } };
next(); }
/** * @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(/** @type {IDBTransaction} */ (e.target).error); transaction.onabort = () => reject(new Error('Transaction aborted')); transaction.oncomplete = () => resolve(); return transaction; }}