diff options
| author | Alex Yatskov <alex@foosoft.net> | 2019-12-03 18:30:22 -0800 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2019-12-03 18:30:22 -0800 | 
| commit | f9ea6206550ceee625ea93215a6e08d45a750086 (patch) | |
| tree | 803fe11a788a631076b3fb11a98e50bb8b454396 /ext/bg/js/database.js | |
| parent | 08ad2779678cd447bd747c2b155ef9b5135fdf5d (diff) | |
| parent | 3975aabf4dc283d49ec46d0ed7ead982b9fa7441 (diff) | |
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg/js/database.js')
| -rw-r--r-- | ext/bg/js/database.js | 428 | 
1 files changed, 356 insertions, 72 deletions
| diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 9f477b24..a20d5f15 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -23,63 +23,137 @@ class Database {      }      async prepare() { -        if (this.db) { +        if (this.db !== null) {              throw new Error('Database already initialized');          } -        this.db = new Dexie('dict'); -        this.db.version(2).stores({ -            terms:        '++id,dictionary,expression,reading', -            kanji:        '++,dictionary,character', -            tagMeta:      '++,dictionary', -            dictionaries: '++,title,version' -        }); -        this.db.version(3).stores({ -            termMeta:  '++,dictionary,expression', -            kanjiMeta: '++,dictionary,character', -            tagMeta:   '++,dictionary,name' -        }); -        this.db.version(4).stores({ -            terms: '++id,dictionary,expression,reading,sequence' -        }); - -        await this.db.open(); +        try { +            this.db = await Database.open('dict', 4, (db, transaction, oldVersion) => { +                Database.upgrade(db, transaction, oldVersion, [ +                    { +                        version: 2, +                        stores: { +                            terms: { +                                primaryKey: {keyPath: 'id', autoIncrement: true}, +                                indices: ['dictionary', 'expression', 'reading'] +                            }, +                            kanji: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['dictionary', 'character'] +                            }, +                            tagMeta: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['dictionary'] +                            }, +                            dictionaries: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['title', 'version'] +                            } +                        } +                    }, +                    { +                        version: 3, +                        stores: { +                            termMeta: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['dictionary', 'expression'] +                            }, +                            kanjiMeta: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['dictionary', 'character'] +                            }, +                            tagMeta: { +                                primaryKey: {autoIncrement: true}, +                                indices: ['dictionary', 'name'] +                            } +                        } +                    }, +                    { +                        version: 4, +                        stores: { +                            terms: { +                                primaryKey: {keyPath: 'id', autoIncrement: true}, +                                indices: ['dictionary', 'expression', 'reading', 'sequence'] +                            } +                        } +                    } +                ]); +            }); +            return true; +        } catch (e) { +            console.error(e); +            return false; +        }      }      async purge() {          this.validate();          this.db.close(); -        await this.db.delete(); +        await Database.deleteDatabase(this.db.name);          this.db = null;          await this.prepare();      } -    async findTermsBulk(termList, titles) { +    async deleteDictionary(dictionaryName, onProgress, progressSettings) { +        this.validate(); + +        const targets = [ +            ['dictionaries', 'title'], +            ['kanji', 'dictionary'], +            ['kanjiMeta', 'dictionary'], +            ['terms', 'dictionary'], +            ['termMeta', 'dictionary'], +            ['tagMeta', 'dictionary'] +        ]; +        const promises = []; +        const progressData = { +            count: 0, +            processed: 0, +            storeCount: targets.length, +            storesProcesed: 0 +        }; +        let progressRate = (typeof progressSettings === 'object' && progressSettings !== null ? progressSettings.rate : 0); +        if (typeof progressRate !== 'number' || progressRate <= 0) { +            progressRate = 1000; +        } + +        for (const [objectStoreName, index] of targets) { +            const dbTransaction = this.db.transaction([objectStoreName], 'readwrite'); +            const dbObjectStore = dbTransaction.objectStore(objectStoreName); +            const dbIndex = dbObjectStore.index(index); +            const only = IDBKeyRange.only(dictionaryName); +            promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate)); +        } + +        await Promise.all(promises); +    } + +    async findTermsBulk(termList, titles, wildcard) {          this.validate();          const promises = [];          const visited = {};          const results = [];          const processRow = (row, index) => { -            if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) { +            if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) {                  visited[row.id] = true;                  results.push(Database.createTerm(row, index));              }          }; -        const db = this.db.backendDB(); -        const dbTransaction = db.transaction(['terms'], 'readonly'); +        const dbTransaction = this.db.transaction(['terms'], 'readonly');          const dbTerms = dbTransaction.objectStore('terms');          const dbIndex1 = dbTerms.index('expression');          const dbIndex2 = dbTerms.index('reading');          for (let i = 0; i < termList.length; ++i) { -            const only = IDBKeyRange.only(termList[i]); +            const term = termList[i]; +            const query = wildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);              promises.push( -                Database.getAll(dbIndex1, only, i, processRow), -                Database.getAll(dbIndex2, only, i, processRow) +                Database.getAll(dbIndex1, query, i, processRow), +                Database.getAll(dbIndex2, query, i, processRow)              );          } @@ -99,8 +173,7 @@ class Database {              }          }; -        const db = this.db.backendDB(); -        const dbTransaction = db.transaction(['terms'], 'readonly'); +        const dbTransaction = this.db.transaction(['terms'], 'readonly');          const dbTerms = dbTransaction.objectStore('terms');          const dbIndex = dbTerms.index('expression'); @@ -125,8 +198,7 @@ class Database {              }          }; -        const db = this.db.backendDB(); -        const dbTransaction = db.transaction(['terms'], 'readonly'); +        const dbTransaction = this.db.transaction(['terms'], 'readonly');          const dbTerms = dbTransaction.objectStore('terms');          const dbIndex = dbTerms.index('sequence'); @@ -163,8 +235,7 @@ class Database {              }          }; -        const db = this.db.backendDB(); -        const dbTransaction = db.transaction([tableName], 'readonly'); +        const dbTransaction = this.db.transaction([tableName], 'readonly');          const dbTerms = dbTransaction.objectStore(tableName);          const dbIndex = dbTerms.index(indexName); @@ -182,12 +253,11 @@ class Database {          this.validate();          let result = null; -        const db = this.db.backendDB(); -        const dbTransaction = db.transaction(['tagMeta'], 'readonly'); +        const dbTransaction = this.db.transaction(['tagMeta'], 'readonly');          const dbTerms = dbTransaction.objectStore('tagMeta');          const dbIndex = dbTerms.index('name');          const only = IDBKeyRange.only(name); -        await Database.getAll(dbIndex, only, null, row => { +        await Database.getAll(dbIndex, only, null, (row) => {              if (title === row.dictionary) {                  result = row;              } @@ -196,24 +266,76 @@ class Database {          return result;      } -    async summarize() { +    async getDictionaryInfo() { +        this.validate(); + +        const results = []; +        const dbTransaction = this.db.transaction(['dictionaries'], 'readonly'); +        const dbDictionaries = dbTransaction.objectStore('dictionaries'); + +        await Database.getAll(dbDictionaries, null, null, (info) => results.push(info)); + +        return results; +    } + +    async getDictionaryCounts(dictionaryNames, getTotal) {          this.validate(); -        return this.db.dictionaries.toArray(); +        const objectStoreNames = [ +            'kanji', +            'kanjiMeta', +            'terms', +            'termMeta', +            'tagMeta' +        ]; +        const dbCountTransaction = this.db.transaction(objectStoreNames, 'readonly'); + +        const targets = []; +        for (const objectStoreName of objectStoreNames) { +            targets.push([ +                objectStoreName, +                dbCountTransaction.objectStore(objectStoreName).index('dictionary') +            ]); +        } + +        // Query is required for Edge, otherwise index.count throws an exception. +        const query1 = IDBKeyRange.lowerBound('', false); +        const totalPromise = getTotal ? Database.getCounts(targets, query1) : null; + +        const counts = []; +        const countPromises = []; +        for (let i = 0; i < dictionaryNames.length; ++i) { +            counts.push(null); +            const index = i; +            const query2 = IDBKeyRange.only(dictionaryNames[i]); +            const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v); +            countPromises.push(countPromise); +        } +        await Promise.all(countPromises); + +        const result = {counts}; +        if (totalPromise !== null) { +            result.total = await totalPromise; +        } +        return result;      }      async importDictionary(archive, progressCallback, exceptions) {          this.validate();          const maxTransactionLength = 1000; -        const bulkAdd = async (table, items, total, current) => { -            if (items.length < maxTransactionLength) { +        const bulkAdd = async (objectStoreName, items, total, current) => { +            const db = this.db; +            for (let i = 0; i < items.length; i += maxTransactionLength) {                  if (progressCallback) { -                    progressCallback(total, current); +                    progressCallback(total, current + i / items.length);                  }                  try { -                    await table.bulkAdd(items); +                    const count = Math.min(maxTransactionLength, items.length - i); +                    const transaction = db.transaction([objectStoreName], 'readwrite'); +                    const objectStore = transaction.objectStore(objectStoreName); +                    await Database.bulkAdd(objectStore, items, i, count);                  } catch (e) {                      if (exceptions) {                          exceptions.push(e); @@ -221,37 +343,27 @@ class Database {                          throw e;                      }                  } -            } else { -                for (let i = 0; i < items.length; i += maxTransactionLength) { -                    if (progressCallback) { -                        progressCallback(total, current + i / items.length); -                    } - -                    let count = Math.min(maxTransactionLength, items.length - i); -                    try { -                        await table.bulkAdd(items.slice(i, i + count)); -                    } catch (e) { -                        if (exceptions) { -                            exceptions.push(e); -                        } else { -                            throw e; -                        } -                    } -                }              }          }; -        const indexDataLoaded = async summary => { +        const indexDataLoaded = async (summary) => {              if (summary.version > 3) {                  throw new Error('Unsupported dictionary version');              } -            const count = await this.db.dictionaries.where('title').equals(summary.title).count(); +            const db = this.db; +            const dbCountTransaction = db.transaction(['dictionaries'], 'readonly'); +            const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title'); +            const only = IDBKeyRange.only(summary.title); +            const count = await Database.getCount(dbIndex, only); +              if (count > 0) {                  throw new Error('Dictionary is already imported');              } -            await this.db.dictionaries.add(summary); +            const transaction = db.transaction(['dictionaries'], 'readwrite'); +            const objectStore = transaction.objectStore('dictionaries'); +            await Database.bulkAdd(objectStore, [summary], 0, 1);          };          const termDataLoaded = async (summary, entries, total, current) => { @@ -284,7 +396,7 @@ class Database {                  }              } -            await bulkAdd(this.db.terms, rows, total, current); +            await bulkAdd('terms', rows, total, current);          };          const termMetaDataLoaded = async (summary, entries, total, current) => { @@ -298,7 +410,7 @@ class Database {                  });              } -            await bulkAdd(this.db.termMeta, rows, total, current); +            await bulkAdd('termMeta', rows, total, current);          };          const kanjiDataLoaded = async (summary, entries, total, current)  => { @@ -328,7 +440,7 @@ class Database {                  }              } -            await bulkAdd(this.db.kanji, rows, total, current); +            await bulkAdd('kanji', rows, total, current);          };          const kanjiMetaDataLoaded = async (summary, entries, total, current) => { @@ -342,7 +454,7 @@ class Database {                  });              } -            await bulkAdd(this.db.kanjiMeta, rows, total, current); +            await bulkAdd('kanjiMeta', rows, total, current);          };          const tagDataLoaded = async (summary, entries, total, current) => { @@ -360,7 +472,7 @@ class Database {                  rows.push(row);              } -            await bulkAdd(this.db.tagMeta, rows, total, current); +            await bulkAdd('tagMeta', rows, total, current);          };          return await Database.importDictionaryZip( @@ -410,13 +522,13 @@ class Database {          await indexDataLoaded(summary); -        const buildTermBankName      = index => `term_bank_${index + 1}.json`; -        const buildTermMetaBankName  = index => `term_meta_bank_${index + 1}.json`; -        const buildKanjiBankName     = index => `kanji_bank_${index + 1}.json`; -        const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`; -        const buildTagBankName       = index => `tag_bank_${index + 1}.json`; +        const buildTermBankName      = (index) => `term_bank_${index + 1}.json`; +        const buildTermMetaBankName  = (index) => `term_meta_bank_${index + 1}.json`; +        const buildKanjiBankName     = (index) => `kanji_bank_${index + 1}.json`; +        const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`; +        const buildTagBankName       = (index) => `tag_bank_${index + 1}.json`; -        const countBanks = namer => { +        const countBanks = (namer) => {              let count = 0;              while (zip.files[namer(count)]) {                  ++count; @@ -539,4 +651,176 @@ class Database {              };          });      } + +    static getCounts(targets, query) { +        const countPromises = []; +        const counts = {}; +        for (const [objectStoreName, index] of targets) { +            const n = objectStoreName; +            const countPromise = Database.getCount(index, query).then((count) => counts[n] = count); +            countPromises.push(countPromise); +        } +        return Promise.all(countPromises).then(() => counts); +    } + +    static getCount(dbIndex, query) { +        return new Promise((resolve, reject) => { +            const request = dbIndex.count(query); +            request.onerror = (e) => reject(e); +            request.onsuccess = (e) => resolve(e.target.result); +        }); +    } + +    static getAllKeys(dbIndex, query) { +        const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor; +        return fn(dbIndex, query); +    } + +    static getAllKeysFast(dbIndex, query) { +        return new Promise((resolve, reject) => { +            const request = dbIndex.getAllKeys(query); +            request.onerror = (e) => reject(e); +            request.onsuccess = (e) => resolve(e.target.result); +        }); +    } + +    static getAllKeysUsingCursor(dbIndex, query) { +        return new Promise((resolve, reject) => { +            const primaryKeys = []; +            const request = dbIndex.openKeyCursor(query, 'next'); +            request.onerror = (e) => reject(e); +            request.onsuccess = (e) => { +                const cursor = e.target.result; +                if (cursor) { +                    primaryKeys.push(cursor.primaryKey); +                    cursor.continue(); +                } else { +                    resolve(primaryKeys); +                } +            }; +        }); +    } + +    static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) { +        const hasProgress = (typeof onProgress === 'function'); +        const count = await Database.getCount(dbIndex, query); +        ++progressData.storesProcesed; +        progressData.count += count; +        if (hasProgress) { +            onProgress(progressData); +        } + +        const onValueDeleted = ( +            hasProgress ? +            () => { +                const p = ++progressData.processed; +                if ((p % progressRate) === 0 || p === progressData.count) { +                    onProgress(progressData); +                } +            } : +            () => {} +        ); + +        const promises = []; +        const primaryKeys = await Database.getAllKeys(dbIndex, query); +        for (const key of primaryKeys) { +            const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted); +            promises.push(promise); +        } + +        await Promise.all(promises); +    } + +    static deleteValue(dbObjectStore, key) { +        return new Promise((resolve, reject) => { +            const request = dbObjectStore.delete(key); +            request.onerror = (e) => reject(e); +            request.onsuccess = () => resolve(); +        }); +    } + +    static bulkAdd(objectStore, items, start, count) { +        return new Promise((resolve, reject) => { +            if (start + count > items.length) { +                count = items.length - start; +            } + +            if (count <= 0) { +                resolve(); +                return; +            } + +            const end = start + count; +            let completedCount = 0; +            const onError = (e) => reject(e); +            const onSuccess = () => { +                if (++completedCount >= count) { +                    resolve(); +                } +            }; + +            for (let i = start; i < end; ++i) { +                const request = objectStore.add(items[i]); +                request.onerror = onError; +                request.onsuccess = onSuccess; +            } +        }); +    } + +    static open(name, version, onUpgradeNeeded) { +        return new Promise((resolve, reject) => { +            const request = window.indexedDB.open(name, version * 10); + +            request.onupgradeneeded = (event) => { +                try { +                    request.transaction.onerror = (e) => reject(e); +                    onUpgradeNeeded(request.result, request.transaction, event.oldVersion / 10, event.newVersion / 10); +                } catch (e) { +                    reject(e); +                } +            }; + +            request.onerror = (e) => reject(e); +            request.onsuccess = () => resolve(request.result); +        }); +    } + +    static upgrade(db, transaction, oldVersion, upgrades) { +        for (const {version, stores} of upgrades) { +            if (oldVersion >= version) { continue; } + +            const objectStoreNames = Object.keys(stores); +            for (const objectStoreName of objectStoreNames) { +                const {primaryKey, indices} = stores[objectStoreName]; + +                const objectStoreNames = transaction.objectStoreNames || db.objectStoreNames; +                const objectStore = ( +                    Database.listContains(objectStoreNames, objectStoreName) ? +                    transaction.objectStore(objectStoreName) : +                    db.createObjectStore(objectStoreName, primaryKey) +                ); + +                for (const indexName of indices) { +                    if (Database.listContains(objectStore.indexNames, indexName)) { continue; } + +                    objectStore.createIndex(indexName, indexName, {}); +                } +            } +        } +    } + +    static deleteDatabase(dbName) { +        return new Promise((resolve, reject) => { +            const request = indexedDB.deleteDatabase(dbName); +            request.onerror = (e) => reject(e); +            request.onsuccess = () => resolve(); +        }); +    } + +    static listContains(list, value) { +        for (let i = 0, ii = list.length; i < ii; ++i) { +            if (list[i] === value) { return true; } +        } +        return false; +    }  } |