2016-03-20 02:32:35 +00:00
/ *
2020-01-01 17:00:00 +00:00
* Copyright ( C ) 2016 - 2020 Alex Yatskov < alex @ foosoft . net >
2016-03-20 02:32:35 +00:00
* Author : Alex Yatskov < alex @ foosoft . net >
*
* 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
2020-01-01 17:00:31 +00:00
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
2016-03-20 02:32:35 +00:00
* /
2020-03-11 02:30:36 +00:00
/ * g l o b a l
* JSZip
* JsonSchema
* dictFieldSplit
* requestJson
* /
2016-03-20 02:32:35 +00:00
2016-11-07 16:29:21 +00:00
class Database {
2016-03-21 00:15:40 +00:00
constructor ( ) {
2016-09-12 05:47:08 +00:00
this . db = null ;
2020-02-18 00:43:44 +00:00
this . _schemas = new Map ( ) ;
2016-08-21 20:32:36 +00:00
}
2020-02-17 21:16:08 +00:00
// Public
2017-07-10 20:16:24 +00:00
async prepare ( ) {
2019-11-03 21:13:40 +00:00
if ( this . db !== null ) {
2019-10-08 00:46:02 +00:00
throw new Error ( 'Database already initialized' ) ;
2016-09-13 22:59:18 +00:00
}
2019-11-10 01:48:30 +00:00
try {
2020-02-17 21:16:08 +00:00
this . db = await Database . _open ( 'dict' , 5 , ( db , transaction , oldVersion ) => {
Database . _upgrade ( db , transaction , oldVersion , [
2019-11-10 01:48:30 +00:00
{
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' ]
}
2019-11-03 20:40:56 +00:00
}
2019-11-10 01:48:30 +00:00
} ,
{
version : 3 ,
stores : {
termMeta : {
primaryKey : { autoIncrement : true } ,
indices : [ 'dictionary' , 'expression' ]
} ,
kanjiMeta : {
primaryKey : { autoIncrement : true } ,
indices : [ 'dictionary' , 'character' ]
} ,
tagMeta : {
primaryKey : { autoIncrement : true } ,
indices : [ 'dictionary' , 'name' ]
}
2019-11-03 20:40:56 +00:00
}
2019-11-10 01:48:30 +00:00
} ,
{
version : 4 ,
stores : {
terms : {
primaryKey : { keyPath : 'id' , autoIncrement : true } ,
indices : [ 'dictionary' , 'expression' , 'reading' , 'sequence' ]
}
2019-11-03 20:40:56 +00:00
}
2019-11-24 02:48:24 +00:00
} ,
{
version : 5 ,
stores : {
terms : {
primaryKey : { keyPath : 'id' , autoIncrement : true } ,
indices : [ 'dictionary' , 'expression' , 'reading' , 'sequence' , 'expressionReverse' , 'readingReverse' ]
}
}
2019-11-03 20:40:56 +00:00
}
2019-11-10 01:48:30 +00:00
] ) ;
} ) ;
return true ;
} catch ( e ) {
console . error ( e ) ;
return false ;
}
2016-03-21 00:15:40 +00:00
}
2020-02-20 00:59:24 +00:00
async close ( ) {
2020-02-22 17:45:50 +00:00
this . _validate ( ) ;
2020-02-20 00:59:24 +00:00
this . db . close ( ) ;
this . db = null ;
}
2017-07-10 20:16:24 +00:00
async purge ( ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2016-11-14 03:10:28 +00:00
this . db . close ( ) ;
2020-02-17 21:16:08 +00:00
await Database . _deleteDatabase ( this . db . name ) ;
2017-07-10 20:16:24 +00:00
this . db = null ;
await this . prepare ( ) ;
2016-11-14 03:10:28 +00:00
}
2019-11-02 20:21:06 +00:00
async deleteDictionary ( dictionaryName , onProgress , progressSettings ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-11-02 20:21:06 +00:00
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 ) {
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ objectStoreName ] , 'readwrite' ) ;
2019-11-02 20:21:06 +00:00
const dbObjectStore = dbTransaction . objectStore ( objectStoreName ) ;
const dbIndex = dbObjectStore . index ( index ) ;
const only = IDBKeyRange . only ( dictionaryName ) ;
2020-02-17 21:16:08 +00:00
promises . push ( Database . _deleteValues ( dbObjectStore , dbIndex , only , onProgress , progressData , progressRate ) ) ;
2019-11-02 20:21:06 +00:00
}
await Promise . all ( promises ) ;
}
2020-02-15 20:01:21 +00:00
async findTermsBulk ( termList , dictionaries , wildcard ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-10-19 03:04:06 +00:00
2019-08-31 01:06:21 +00:00
const promises = [ ] ;
2020-02-15 17:31:11 +00:00
const visited = new Set ( ) ;
2019-08-31 01:06:21 +00:00
const results = [ ] ;
2019-10-08 02:28:12 +00:00
const processRow = ( row , index ) => {
2020-02-15 20:01:21 +00:00
if ( dictionaries . has ( row . dictionary ) && ! visited . has ( row . id ) ) {
2020-02-15 17:31:11 +00:00
visited . add ( row . id ) ;
2020-02-17 21:16:08 +00:00
results . push ( Database . _createTerm ( row , index ) ) ;
2019-10-08 02:28:12 +00:00
}
} ;
2019-08-31 01:06:21 +00:00
2019-11-24 02:48:24 +00:00
const useWildcard = ! ! wildcard ;
const prefixWildcard = wildcard === 'prefix' ;
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ 'terms' ] , 'readonly' ) ;
2019-08-31 01:06:21 +00:00
const dbTerms = dbTransaction . objectStore ( 'terms' ) ;
2019-11-24 02:48:24 +00:00
const dbIndex1 = dbTerms . index ( prefixWildcard ? 'expressionReverse' : 'expression' ) ;
const dbIndex2 = dbTerms . index ( prefixWildcard ? 'readingReverse' : 'reading' ) ;
2019-08-31 01:06:21 +00:00
2019-10-19 14:09:18 +00:00
for ( let i = 0 ; i < termList . length ; ++ i ) {
2019-11-24 02:48:24 +00:00
const term = prefixWildcard ? stringReverse ( termList [ i ] ) : termList [ i ] ;
const query = useWildcard ? IDBKeyRange . bound ( term , ` ${ term } \u ffff ` , false , false ) : IDBKeyRange . only ( term ) ;
2019-08-31 01:06:21 +00:00
promises . push (
2020-02-17 21:16:08 +00:00
Database . _getAll ( dbIndex1 , query , i , processRow ) ,
Database . _getAll ( dbIndex2 , query , i , processRow )
2019-08-31 01:06:21 +00:00
) ;
}
await Promise . all ( promises ) ;
return results ;
}
2020-02-15 20:01:21 +00:00
async findTermsExactBulk ( termList , readingList , dictionaries ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-10-19 14:09:18 +00:00
const promises = [ ] ;
const results = [ ] ;
const processRow = ( row , index ) => {
2020-02-15 20:01:21 +00:00
if ( row . reading === readingList [ index ] && dictionaries . has ( row . dictionary ) ) {
2020-02-17 21:16:08 +00:00
results . push ( Database . _createTerm ( row , index ) ) ;
2019-10-19 14:09:18 +00:00
}
} ;
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ 'terms' ] , 'readonly' ) ;
2019-10-19 14:09:18 +00:00
const dbTerms = dbTransaction . objectStore ( 'terms' ) ;
const dbIndex = dbTerms . index ( 'expression' ) ;
for ( let i = 0 ; i < termList . length ; ++ i ) {
const only = IDBKeyRange . only ( termList [ i ] ) ;
2020-02-17 21:16:08 +00:00
promises . push ( Database . _getAll ( dbIndex , only , i , processRow ) ) ;
2019-10-19 14:09:18 +00:00
}
await Promise . all ( promises ) ;
return results ;
}
async findTermsBySequenceBulk ( sequenceList , mainDictionary ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-10-19 03:04:06 +00:00
2019-08-31 01:06:21 +00:00
const promises = [ ] ;
const results = [ ] ;
2019-10-08 02:28:12 +00:00
const processRow = ( row , index ) => {
2019-10-19 14:09:18 +00:00
if ( row . dictionary === mainDictionary ) {
2020-02-17 21:16:08 +00:00
results . push ( Database . _createTerm ( row , index ) ) ;
2019-10-08 02:28:12 +00:00
}
} ;
2019-08-31 01:06:21 +00:00
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ 'terms' ] , 'readonly' ) ;
2019-10-19 14:09:18 +00:00
const dbTerms = dbTransaction . objectStore ( 'terms' ) ;
const dbIndex = dbTerms . index ( 'sequence' ) ;
2019-08-31 01:06:21 +00:00
2019-10-19 14:09:18 +00:00
for ( let i = 0 ; i < sequenceList . length ; ++ i ) {
const only = IDBKeyRange . only ( sequenceList [ i ] ) ;
2020-02-17 21:16:08 +00:00
promises . push ( Database . _getAll ( dbIndex , only , i , processRow ) ) ;
2019-08-31 01:06:21 +00:00
}
await Promise . all ( promises ) ;
return results ;
}
2020-02-15 20:01:21 +00:00
async findTermMetaBulk ( termList , dictionaries ) {
return this . _findGenericBulk ( 'termMeta' , 'expression' , termList , dictionaries , Database . _createTermMeta ) ;
2019-10-19 14:09:18 +00:00
}
2020-02-15 20:01:21 +00:00
async findKanjiBulk ( kanjiList , dictionaries ) {
return this . _findGenericBulk ( 'kanji' , 'character' , kanjiList , dictionaries , Database . _createKanji ) ;
2019-10-19 14:09:18 +00:00
}
2020-02-15 20:01:21 +00:00
async findKanjiMetaBulk ( kanjiList , dictionaries ) {
return this . _findGenericBulk ( 'kanjiMeta' , 'character' , kanjiList , dictionaries , Database . _createKanjiMeta ) ;
2019-10-19 14:09:18 +00:00
}
2017-09-13 03:20:03 +00:00
2017-09-14 01:03:55 +00:00
async findTagForTitle ( name , title ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2016-09-12 05:47:08 +00:00
2019-08-30 23:38:36 +00:00
let result = null ;
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ 'tagMeta' ] , 'readonly' ) ;
2019-10-19 17:41:18 +00:00
const dbTerms = dbTransaction . objectStore ( 'tagMeta' ) ;
const dbIndex = dbTerms . index ( 'name' ) ;
const only = IDBKeyRange . only ( name ) ;
2020-02-17 21:16:08 +00:00
await Database . _getAll ( dbIndex , only , null , ( row ) => {
2019-08-30 23:38:36 +00:00
if ( title === row . dictionary ) {
result = row ;
}
} ) ;
2017-09-14 00:26:02 +00:00
2017-09-13 23:42:04 +00:00
return result ;
2016-08-24 05:22:09 +00:00
}
2016-08-24 03:33:04 +00:00
2019-11-01 01:10:56 +00:00
async getDictionaryInfo ( ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-11-01 01:10:56 +00:00
const results = [ ] ;
2019-11-03 21:13:40 +00:00
const dbTransaction = this . db . transaction ( [ 'dictionaries' ] , 'readonly' ) ;
2019-11-01 01:10:56 +00:00
const dbDictionaries = dbTransaction . objectStore ( 'dictionaries' ) ;
2020-02-17 21:16:08 +00:00
await Database . _getAll ( dbDictionaries , null , null , ( info ) => results . push ( info ) ) ;
2019-11-01 01:10:56 +00:00
return results ;
}
async getDictionaryCounts ( dictionaryNames , getTotal ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
2019-11-01 01:10:56 +00:00
const objectStoreNames = [
'kanji' ,
'kanjiMeta' ,
'terms' ,
'termMeta' ,
'tagMeta'
] ;
2019-11-03 21:13:40 +00:00
const dbCountTransaction = this . db . transaction ( objectStoreNames , 'readonly' ) ;
2019-11-01 01:10:56 +00:00
const targets = [ ] ;
for ( const objectStoreName of objectStoreNames ) {
targets . push ( [
objectStoreName ,
dbCountTransaction . objectStore ( objectStoreName ) . index ( 'dictionary' )
] ) ;
}
2019-11-10 01:49:44 +00:00
// Query is required for Edge, otherwise index.count throws an exception.
const query1 = IDBKeyRange . lowerBound ( '' , false ) ;
2020-02-17 21:16:08 +00:00
const totalPromise = getTotal ? Database . _getCounts ( targets , query1 ) : null ;
2019-11-01 01:10:56 +00:00
const counts = [ ] ;
const countPromises = [ ] ;
for ( let i = 0 ; i < dictionaryNames . length ; ++ i ) {
counts . push ( null ) ;
const index = i ;
2019-11-10 01:49:44 +00:00
const query2 = IDBKeyRange . only ( dictionaryNames [ i ] ) ;
2020-02-17 21:16:08 +00:00
const countPromise = Database . _getCounts ( targets , query2 ) . then ( ( v ) => counts [ index ] = v ) ;
2019-11-01 01:10:56 +00:00
countPromises . push ( countPromise ) ;
}
await Promise . all ( countPromises ) ;
const result = { counts } ;
if ( totalPromise !== null ) {
result . total = await totalPromise ;
}
return result ;
}
2020-02-17 23:19:11 +00:00
async importDictionary ( archiveSource , onProgress , details ) {
2020-02-17 23:17:12 +00:00
this . _validate ( ) ;
const db = this . db ;
const hasOnProgress = ( typeof onProgress === 'function' ) ;
// Read archive
const archive = await JSZip . loadAsync ( archiveSource ) ;
// Read and validate index
2020-02-22 18:25:28 +00:00
const indexFileName = 'index.json' ;
const indexFile = archive . files [ indexFileName ] ;
2020-02-17 23:17:12 +00:00
if ( ! indexFile ) {
throw new Error ( 'No dictionary index found in archive' ) ;
}
const index = JSON . parse ( await indexFile . async ( 'string' ) ) ;
2020-02-18 00:43:44 +00:00
const indexSchema = await this . _getSchema ( '/bg/data/dictionary-index-schema.json' ) ;
2020-02-22 18:25:28 +00:00
Database . _validateJsonSchema ( index , indexSchema , indexFileName ) ;
2020-02-18 00:43:44 +00:00
2020-02-17 23:17:12 +00:00
const dictionaryTitle = index . title ;
const version = index . format || index . version ;
if ( ! dictionaryTitle || ! index . revision ) {
throw new Error ( 'Unrecognized dictionary format' ) ;
}
// Verify database is not already imported
if ( await this . _dictionaryExists ( dictionaryTitle ) ) {
throw new Error ( 'Dictionary is already imported' ) ;
}
// Data format converters
const convertTermBankEntry = ( entry ) => {
if ( version === 1 ) {
const [ expression , reading , definitionTags , rules , score , ... glossary ] = entry ;
return { expression , reading , definitionTags , rules , score , glossary } ;
} else {
const [ expression , reading , definitionTags , rules , score , glossary , sequence , termTags ] = entry ;
return { expression , reading , definitionTags , rules , score , glossary , sequence , termTags } ;
}
} ;
const convertTermMetaBankEntry = ( entry ) => {
const [ expression , mode , data ] = entry ;
return { expression , mode , data } ;
} ;
const convertKanjiBankEntry = ( entry ) => {
if ( version === 1 ) {
const [ character , onyomi , kunyomi , tags , ... meanings ] = entry ;
return { character , onyomi , kunyomi , tags , meanings } ;
} else {
const [ character , onyomi , kunyomi , tags , m eanings , stats ] = entry ;
return { character , onyomi , kunyomi , tags , meanings , stats } ;
}
} ;
const convertKanjiMetaBankEntry = ( entry ) => {
const [ character , mode , data ] = entry ;
return { character , mode , data } ;
} ;
const convertTagBankEntry = ( entry ) => {
const [ name , category , order , notes , score ] = entry ;
return { name , category , order , notes , score } ;
} ;
// Archive file reading
2020-02-18 00:43:44 +00:00
const readFileSequence = async ( fileNameFormat , convertEntry , schema ) => {
2020-02-17 23:17:12 +00:00
const results = [ ] ;
for ( let i = 1 ; true ; ++ i ) {
const fileName = fileNameFormat . replace ( /\?/ , ` ${ i } ` ) ;
const file = archive . files [ fileName ] ;
if ( ! file ) { break ; }
const entries = JSON . parse ( await file . async ( 'string' ) ) ;
2020-02-22 18:25:28 +00:00
Database . _validateJsonSchema ( entries , schema , fileName ) ;
2020-02-18 00:43:44 +00:00
2020-02-17 23:17:12 +00:00
for ( let entry of entries ) {
entry = convertEntry ( entry ) ;
entry . dictionary = dictionaryTitle ;
results . push ( entry ) ;
}
}
return results ;
} ;
2020-02-18 00:43:44 +00:00
// Load schemas
const dataBankSchemaPaths = this . constructor . _getDataBankSchemaPaths ( version ) ;
const dataBankSchemas = await Promise . all ( dataBankSchemaPaths . map ( ( path ) => this . _getSchema ( path ) ) ) ;
2020-02-17 23:17:12 +00:00
// Load data
2020-02-18 00:43:44 +00:00
const termList = await readFileSequence ( 'term_bank_?.json' , convertTermBankEntry , dataBankSchemas [ 0 ] ) ;
const termMetaList = await readFileSequence ( 'term_meta_bank_?.json' , convertTermMetaBankEntry , dataBankSchemas [ 1 ] ) ;
const kanjiList = await readFileSequence ( 'kanji_bank_?.json' , convertKanjiBankEntry , dataBankSchemas [ 2 ] ) ;
const kanjiMetaList = await readFileSequence ( 'kanji_meta_bank_?.json' , convertKanjiMetaBankEntry , dataBankSchemas [ 3 ] ) ;
const tagList = await readFileSequence ( 'tag_bank_?.json' , convertTagBankEntry , dataBankSchemas [ 4 ] ) ;
2020-02-17 23:17:12 +00:00
// Old tags
const indexTagMeta = index . tagMeta ;
if ( typeof indexTagMeta === 'object' && indexTagMeta !== null ) {
for ( const name of Object . keys ( indexTagMeta ) ) {
const { category , order , notes , score } = indexTagMeta [ name ] ;
tagList . push ( { name , category , order , notes , score } ) ;
}
}
// Prefix wildcard support
const prefixWildcardsSupported = ! ! details . prefixWildcardsSupported ;
if ( prefixWildcardsSupported ) {
for ( const entry of termList ) {
entry . expressionReverse = stringReverse ( entry . expression ) ;
entry . readingReverse = stringReverse ( entry . reading ) ;
}
}
// Add dictionary
const summary = {
title : dictionaryTitle ,
revision : index . revision ,
sequenced : index . sequenced ,
version ,
prefixWildcardsSupported
} ;
{
const transaction = db . transaction ( [ 'dictionaries' ] , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'dictionaries' ) ;
await Database . _bulkAdd ( objectStore , [ summary ] , 0 , 1 ) ;
}
// Add data
const errors = [ ] ;
const total = (
termList . length +
termMetaList . length +
kanjiList . length +
kanjiMetaList . length +
tagList . length
) ;
let loadedCount = 0 ;
const maxTransactionLength = 1000 ;
const bulkAdd = async ( objectStoreName , entries ) => {
const ii = entries . length ;
for ( let i = 0 ; i < ii ; i += maxTransactionLength ) {
const count = Math . min ( maxTransactionLength , ii - i ) ;
try {
const transaction = db . transaction ( [ objectStoreName ] , 'readwrite' ) ;
const objectStore = transaction . objectStore ( objectStoreName ) ;
await Database . _bulkAdd ( objectStore , entries , i , count ) ;
} catch ( e ) {
errors . push ( e ) ;
}
loadedCount += count ;
if ( hasOnProgress ) {
onProgress ( total , loadedCount ) ;
}
}
} ;
await bulkAdd ( 'terms' , termList ) ;
await bulkAdd ( 'termMeta' , termMetaList ) ;
await bulkAdd ( 'kanji' , kanjiList ) ;
await bulkAdd ( 'kanjiMeta' , kanjiMetaList ) ;
await bulkAdd ( 'tagMeta' , tagList ) ;
return { result : summary , errors } ;
}
2020-02-17 21:16:08 +00:00
// Private
_validate ( ) {
2019-10-08 00:46:02 +00:00
if ( this . db === null ) {
throw new Error ( 'Database not initialized' ) ;
}
}
2020-02-18 00:43:44 +00:00
async _getSchema ( fileName ) {
let schemaPromise = this . _schemas . get ( fileName ) ;
if ( typeof schemaPromise !== 'undefined' ) {
return schemaPromise ;
}
schemaPromise = requestJson ( chrome . runtime . getURL ( fileName ) , 'GET' ) ;
this . _schemas . set ( fileName , schemaPromise ) ;
return schemaPromise ;
}
2020-02-22 18:25:28 +00:00
static _validateJsonSchema ( value , schema , fileName ) {
try {
JsonSchema . validate ( value , schema ) ;
} catch ( e ) {
throw Database . _formatSchemaError ( e , fileName ) ;
}
}
static _formatSchemaError ( e , fileName ) {
const valuePathString = Database . _getSchemaErrorPathString ( e . info . valuePath , 'dictionary' ) ;
const schemaPathString = Database . _getSchemaErrorPathString ( e . info . schemaPath , 'schema' ) ;
const e2 = new Error ( ` Dictionary has invalid data in ' ${ fileName } ' for value ' ${ valuePathString } ', validated against ' ${ schemaPathString } ': ${ e . message } ` ) ;
e2 . data = e ;
return e2 ;
}
static _getSchemaErrorPathString ( infoList , base = '' ) {
let result = base ;
for ( const [ part ] of infoList ) {
switch ( typeof part ) {
case 'string' :
if ( result . length > 0 ) {
result += '.' ;
}
result += part ;
break ;
case 'number' :
result += ` [ ${ part } ] ` ;
break ;
}
}
return result ;
}
2020-02-18 00:43:44 +00:00
static _getDataBankSchemaPaths ( version ) {
const termBank = (
version === 1 ?
'/bg/data/dictionary-term-bank-v1-schema.json' :
'/bg/data/dictionary-term-bank-v3-schema.json'
) ;
const termMetaBank = '/bg/data/dictionary-term-meta-bank-v3-schema.json' ;
const kanjiBank = (
version === 1 ?
'/bg/data/dictionary-kanji-bank-v1-schema.json' :
'/bg/data/dictionary-kanji-bank-v3-schema.json'
) ;
const kanjiMetaBank = '/bg/data/dictionary-kanji-meta-bank-v3-schema.json' ;
const tagBank = '/bg/data/dictionary-tag-bank-v3-schema.json' ;
return [ termBank , termMetaBank , kanjiBank , kanjiMetaBank , tagBank ] ;
}
2020-02-17 23:17:12 +00:00
async _dictionaryExists ( title ) {
const db = this . db ;
const dbCountTransaction = db . transaction ( [ 'dictionaries' ] , 'readonly' ) ;
const dbIndex = dbCountTransaction . objectStore ( 'dictionaries' ) . index ( 'title' ) ;
const only = IDBKeyRange . only ( title ) ;
const count = await Database . _getCount ( dbIndex , only ) ;
return count > 0 ;
}
2020-02-15 20:01:21 +00:00
async _findGenericBulk ( tableName , indexName , indexValueList , dictionaries , createResult ) {
2020-02-17 21:16:08 +00:00
this . _validate ( ) ;
const promises = [ ] ;
const results = [ ] ;
const processRow = ( row , index ) => {
2020-02-15 20:01:21 +00:00
if ( dictionaries . has ( row . dictionary ) ) {
2020-02-17 21:16:08 +00:00
results . push ( createResult ( row , index ) ) ;
}
} ;
const dbTransaction = this . db . transaction ( [ tableName ] , 'readonly' ) ;
const dbTerms = dbTransaction . objectStore ( tableName ) ;
const dbIndex = dbTerms . index ( indexName ) ;
for ( let i = 0 ; i < indexValueList . length ; ++ i ) {
const only = IDBKeyRange . only ( indexValueList [ i ] ) ;
promises . push ( Database . _getAll ( dbIndex , only , i , processRow ) ) ;
}
await Promise . all ( promises ) ;
return results ;
}
static _createTerm ( row , index ) {
2019-08-30 23:26:58 +00:00
return {
2019-08-31 01:06:21 +00:00
index ,
2019-08-30 23:26:58 +00:00
expression : row . expression ,
reading : row . reading ,
definitionTags : dictFieldSplit ( row . definitionTags || row . tags || '' ) ,
termTags : dictFieldSplit ( row . termTags || '' ) ,
rules : dictFieldSplit ( row . rules ) ,
glossary : row . glossary ,
score : row . score ,
dictionary : row . dictionary ,
id : row . id ,
sequence : typeof row . sequence === 'undefined' ? - 1 : row . sequence
} ;
}
2019-08-31 01:06:21 +00:00
2020-02-17 21:16:08 +00:00
static _createKanji ( row , index ) {
2019-10-19 03:16:33 +00:00
return {
index ,
character : row . character ,
onyomi : dictFieldSplit ( row . onyomi ) ,
kunyomi : dictFieldSplit ( row . kunyomi ) ,
tags : dictFieldSplit ( row . tags ) ,
glossary : row . meanings ,
stats : row . stats ,
dictionary : row . dictionary
} ;
}
2020-02-17 21:16:08 +00:00
static _createTermMeta ( { expression , mode , data , dictionary } , index ) {
2020-01-25 03:22:40 +00:00
return { expression , mode , data , dictionary , index } ;
}
2020-02-17 21:16:08 +00:00
static _createKanjiMeta ( { character , mode , data , dictionary } , index ) {
2020-01-25 03:22:40 +00:00
return { character , mode , data , dictionary , index } ;
2019-08-31 01:06:21 +00:00
}
2020-02-17 21:16:08 +00:00
static _getAll ( dbIndex , query , context , processRow ) {
const fn = typeof dbIndex . getAll === 'function' ? Database . _getAllFast : Database . _getAllUsingCursor ;
2019-10-08 02:28:12 +00:00
return fn ( dbIndex , query , context , processRow ) ;
2019-08-31 01:06:21 +00:00
}
2020-02-17 21:16:08 +00:00
static _getAllFast ( dbIndex , query , context , processRow ) {
2019-08-31 01:06:21 +00:00
return new Promise ( ( resolve , reject ) => {
const request = dbIndex . getAll ( query ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( e ) => {
for ( const row of e . target . result ) {
2019-10-08 02:28:12 +00:00
processRow ( row , context ) ;
2019-08-31 01:06:21 +00:00
}
resolve ( ) ;
} ;
} ) ;
}
2020-02-17 21:16:08 +00:00
static _getAllUsingCursor ( dbIndex , query , context , processRow ) {
2019-08-31 01:06:21 +00:00
return new Promise ( ( resolve , reject ) => {
const request = dbIndex . openCursor ( query , 'next' ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( e ) => {
const cursor = e . target . result ;
if ( cursor ) {
2019-10-08 02:28:12 +00:00
processRow ( cursor . value , context ) ;
2019-08-31 01:06:21 +00:00
cursor . continue ( ) ;
} else {
resolve ( ) ;
}
} ;
} ) ;
}
2019-11-01 01:10:56 +00:00
2020-02-17 21:16:08 +00:00
static _getCounts ( targets , query ) {
2019-11-01 01:10:56 +00:00
const countPromises = [ ] ;
const counts = { } ;
for ( const [ objectStoreName , index ] of targets ) {
const n = objectStoreName ;
2020-02-17 21:16:08 +00:00
const countPromise = Database . _getCount ( index , query ) . then ( ( count ) => counts [ n ] = count ) ;
2019-11-01 01:10:56 +00:00
countPromises . push ( countPromise ) ;
}
return Promise . all ( countPromises ) . then ( ( ) => counts ) ;
}
2020-02-17 21:16:08 +00:00
static _getCount ( dbIndex , query ) {
2019-11-01 01:10:56 +00:00
return new Promise ( ( resolve , reject ) => {
const request = dbIndex . count ( query ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( e ) => resolve ( e . target . result ) ;
} ) ;
}
2019-11-02 20:21:06 +00:00
2020-02-17 21:16:08 +00:00
static _getAllKeys ( dbIndex , query ) {
const fn = typeof dbIndex . getAllKeys === 'function' ? Database . _getAllKeysFast : Database . _getAllKeysUsingCursor ;
2019-11-02 20:21:06 +00:00
return fn ( dbIndex , query ) ;
}
2020-02-17 21:16:08 +00:00
static _getAllKeysFast ( dbIndex , query ) {
2019-11-02 20:21:06 +00:00
return new Promise ( ( resolve , reject ) => {
const request = dbIndex . getAllKeys ( query ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( e ) => resolve ( e . target . result ) ;
} ) ;
}
2020-02-17 21:16:08 +00:00
static _getAllKeysUsingCursor ( dbIndex , query ) {
2019-11-02 20:21:06 +00:00
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 ) ;
}
} ;
} ) ;
}
2020-02-17 21:16:08 +00:00
static async _deleteValues ( dbObjectStore , dbIndex , query , onProgress , progressData , progressRate ) {
2019-11-02 20:21:06 +00:00
const hasProgress = ( typeof onProgress === 'function' ) ;
2020-02-17 21:16:08 +00:00
const count = await Database . _getCount ( dbIndex , query ) ;
2019-11-02 20:21:06 +00:00
++ 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 = [ ] ;
2020-02-17 21:16:08 +00:00
const primaryKeys = await Database . _getAllKeys ( dbIndex , query ) ;
2019-11-02 20:21:06 +00:00
for ( const key of primaryKeys ) {
2020-02-17 21:16:08 +00:00
const promise = Database . _deleteValue ( dbObjectStore , key ) . then ( onValueDeleted ) ;
2019-11-02 20:21:06 +00:00
promises . push ( promise ) ;
}
await Promise . all ( promises ) ;
}
2020-02-17 21:16:08 +00:00
static _deleteValue ( dbObjectStore , key ) {
2019-11-02 20:21:06 +00:00
return new Promise ( ( resolve , reject ) => {
const request = dbObjectStore . delete ( key ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( ) => resolve ( ) ;
} ) ;
}
2019-11-03 18:24:21 +00:00
2020-02-17 21:16:08 +00:00
static _bulkAdd ( objectStore , items , start , count ) {
2019-11-03 18:24:21 +00:00
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 ;
}
} ) ;
}
2019-11-03 20:40:56 +00:00
2020-02-17 21:16:08 +00:00
static _open ( name , version , onUpgradeNeeded ) {
2019-11-03 20:40:56 +00:00
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 ) ;
} ) ;
}
2020-02-17 21:16:08 +00:00
static _upgrade ( db , transaction , oldVersion , upgrades ) {
2019-11-03 20:40:56 +00:00
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 ] ;
2020-02-17 20:21:30 +00:00
const objectStoreNames2 = transaction . objectStoreNames || db . objectStoreNames ;
2019-11-03 20:40:56 +00:00
const objectStore = (
2020-02-17 20:21:30 +00:00
Database . _listContains ( objectStoreNames2 , objectStoreName ) ?
2019-11-03 20:40:56 +00:00
transaction . objectStore ( objectStoreName ) :
db . createObjectStore ( objectStoreName , primaryKey )
) ;
for ( const indexName of indices ) {
2020-02-17 21:16:08 +00:00
if ( Database . _listContains ( objectStore . indexNames , indexName ) ) { continue ; }
2019-11-03 20:40:56 +00:00
objectStore . createIndex ( indexName , indexName , { } ) ;
}
}
}
}
2019-11-03 20:56:00 +00:00
2020-02-17 21:16:08 +00:00
static _deleteDatabase ( dbName ) {
2019-11-03 20:56:00 +00:00
return new Promise ( ( resolve , reject ) => {
const request = indexedDB . deleteDatabase ( dbName ) ;
request . onerror = ( e ) => reject ( e ) ;
request . onsuccess = ( ) => resolve ( ) ;
} ) ;
}
2019-11-10 01:48:30 +00:00
2020-02-17 21:16:08 +00:00
static _listContains ( list , value ) {
2019-11-10 01:48:30 +00:00
for ( let i = 0 , ii = list . length ; i < ii ; ++ i ) {
if ( list [ i ] === value ) { return true ; }
}
return false ;
}
2016-03-20 02:32:35 +00:00
}