refactor: refactor IDB code

This commit is contained in:
Nolan Lawson 2020-06-03 18:09:40 -07:00
parent fbee4e238e
commit a60e130741
3 changed files with 135 additions and 152 deletions

View File

@ -1,4 +1,3 @@
import { IndexedDBEngine } from './IndexedDBEngine'
import { assertNonEmptyString } from './utils/assertNonEmptyString'
import { warnETag } from './utils/warnETag'
import { assertEmojiBaseData } from './utils/assertEmojiBaseData'
@ -7,21 +6,26 @@ import { DEFAULT_DATA_SOURCE, DEFAULT_LOCALE } from './constants'
import { uniqEmoji } from './utils/uniqEmoji'
import { jsonChecksum } from './utils/jsonChecksum'
import { warnOffline } from './utils/warnOffline'
import { closeDatabase, deleteDatabase, openDatabase } from './databaseLifecycle'
import {
isEmpty, hasData, loadData, getEmojiByGroup,
getEmojiBySearchPrefix, getEmojiByShortcode, getEmojiByUnicode
} from './idbInterface'
export default class Database {
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE } = {}) {
this._dataSource = dataSource
this._locale = locale
this._idbEngine = undefined
this._dbName = `lite-emoji-picker-${this._locale}`
this._db = undefined
this._readyPromise = this._init()
}
async _init () {
this._idbEngine = new IndexedDBEngine(`lite-emoji-picker-${this._locale}`)
await this._idbEngine.open()
this._db = await openDatabase(this._dbName)
const url = this._dataSource
const isEmpty = await this._idbEngine.isEmpty()
if (!isEmpty) {
const empty = await isEmpty(this._db)
if (!empty) {
// just do a simple HEAD request first to see if the eTags match
let headResponse
try {
@ -32,7 +36,7 @@ export default class Database {
}
const eTag = headResponse.headers.get('etag')
warnETag(eTag)
if (eTag && await this._idbEngine.hasData(url, eTag)) {
if (eTag && await hasData(this._db, url, eTag)) {
console.log('Database already populated')
return // fast init, data is already loaded
}
@ -41,7 +45,7 @@ export default class Database {
try {
response = await fetch(this._dataSource)
} catch (e) { // offline fallback
if (!isEmpty) {
if (!empty) {
warnOffline(e)
return
}
@ -57,12 +61,12 @@ export default class Database {
// from the object itself.
eTag = await jsonChecksum(emojiBaseData)
}
if (!isEmpty && await this._idbEngine.hasData(url, eTag)) {
if (!empty && await hasData(this._db, url, eTag)) {
console.log('Database already populated')
return // data already loaded
}
await this._idbEngine.loadData(emojiBaseData, url, eTag)
await loadData(this._db, emojiBaseData, url, eTag)
}
async ready () {
@ -72,43 +76,43 @@ export default class Database {
async getEmojiByGroup (group) {
assertNumber(group)
await this._readyPromise
const emojis = await this._idbEngine.getEmojiByGroup(group)
const emojis = await getEmojiByGroup(this._db, group)
return uniqEmoji(emojis)
}
async getEmojiBySearchPrefix (prefix) {
assertNonEmptyString(prefix)
await this._readyPromise
const emojis = await this._idbEngine.getEmojiBySearchPrefix(prefix)
const emojis = await getEmojiBySearchPrefix(this._db, prefix)
return uniqEmoji(emojis)
}
async getEmojiByShortcode (shortcode) {
assertNonEmptyString(shortcode)
await this._readyPromise
const emojis = await this._idbEngine.getEmojiByShortcode(shortcode)
const emojis = await getEmojiByShortcode(this._db, shortcode)
return uniqEmoji(emojis)
}
async getEmojiByUnicode (unicode) {
assertNonEmptyString(unicode)
await this._readyPromise
return this._idbEngine.getEmojiByUnicode(unicode)
return getEmojiByUnicode(this._db, unicode)
}
async close () {
await this._readyPromise
if (this._idbEngine) {
await this._idbEngine.close()
this._idbEngine = undefined
if (this._db) {
await closeDatabase(this._dbName)
this._db = undefined
}
}
async delete () {
await this._readyPromise
if (this._idbEngine) {
await this._idbEngine.delete()
this._idbEngine = undefined
if (this._db) {
await deleteDatabase(this._dbName)
this._db = undefined
}
}
}

View File

@ -1,132 +0,0 @@
import { closeDatabase, dbPromise, deleteDatabase, get, openDatabase } from './databaseLifecycle'
import {
INDEX_GROUP_AND_ORDER, INDEX_TOKENS, KEY_ETAG, KEY_URL,
MODE_READONLY, MODE_READWRITE,
STORE_EMOJI,
STORE_META
} from './constants'
import { transformEmojiBaseData } from './transformEmojiBaseData'
export class IndexedDBEngine {
constructor (dbName) {
this._db = null
this._dbName = dbName
}
async open () {
this._db = await openDatabase(this._dbName)
}
async isEmpty () {
return !(await get(this._db, STORE_META, KEY_URL))
}
async hasData (url, eTag) {
const [oldETag, oldUrl] = await get(this._db, STORE_META, [KEY_ETAG, KEY_URL])
return (oldETag === eTag && oldUrl === url)
}
async loadData (emojiBaseData, url, eTag) {
const transformedData = transformEmojiBaseData(emojiBaseData)
const [oldETag, oldUrl] = await get(this._db, STORE_META, [KEY_ETAG, KEY_URL])
if (oldETag === eTag && oldUrl === url) {
return
}
await dbPromise(this._db, [STORE_EMOJI, STORE_META], MODE_READWRITE, ([emojiStore, metaStore]) => {
let oldETag
let oldUrl
let oldKeys
let todo = 0
function checkFetched () {
if (++todo === 3) {
onFetched()
}
}
function onFetched () {
if (oldETag === eTag && oldUrl === url) {
// check again within the transaction to guard against concurrency, e.g. multiple browser tabs
return
}
if (oldKeys.length) {
for (const key of oldKeys) {
emojiStore.delete(key)
}
}
insertData()
}
function insertData () {
for (const data of transformedData) {
emojiStore.put(data)
}
metaStore.put(eTag, KEY_ETAG)
metaStore.put(url, KEY_URL)
}
metaStore.get(KEY_ETAG).onsuccess = e => {
oldETag = e.target.result
checkFetched()
}
metaStore.get(KEY_URL).onsuccess = e => {
oldUrl = e.target.result
checkFetched()
}
emojiStore.getAllKeys().onsuccess = e => {
oldKeys = e.target.result
checkFetched()
}
})
}
getEmojiByGroup (group) {
return dbPromise(this._db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true)
emojiStore.index(INDEX_GROUP_AND_ORDER).getAll(range).onsuccess = e => {
cb(e.target.result)
}
})
}
getEmojiBySearchPrefix (prefix) {
prefix = prefix.toLowerCase()
return dbPromise(this._db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.bound(prefix, prefix + '\uffff', false, true)
emojiStore.index(INDEX_TOKENS).getAll(range).onsuccess = e => {
cb(e.target.result)
}
})
}
getEmojiByShortcode (shortcode) {
shortcode = shortcode.toLowerCase()
return dbPromise(this._db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.only(shortcode)
emojiStore.index(INDEX_TOKENS).getAll(range).onsuccess = e => {
// of course, we could add an extra index just for shortcodes, but it seems
// simpler to just re-use the existing tokens index and filter in-memory
const results = e.target.result.filter(emoji => emoji.shortcodes.includes(shortcode))
cb(results[0])
}
})
}
getEmojiByUnicode (unicode) {
return dbPromise(this._db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
emojiStore.get(unicode).onsuccess = e => cb(e.target.result)
})
}
async close () {
await closeDatabase(this._dbName)
this._db = null
}
async delete () {
await deleteDatabase(this._dbName)
this._db = null
}
}

View File

@ -0,0 +1,111 @@
import { dbPromise, get } from './databaseLifecycle'
import {
INDEX_GROUP_AND_ORDER, INDEX_TOKENS, KEY_ETAG, KEY_URL,
MODE_READONLY, MODE_READWRITE,
STORE_EMOJI,
STORE_META
} from './constants'
import { transformEmojiBaseData } from './transformEmojiBaseData'
export async function isEmpty (db) {
return !(await get(db, STORE_META, KEY_URL))
}
export async function hasData (db, url, eTag) {
const [oldETag, oldUrl] = await get(db, STORE_META, [KEY_ETAG, KEY_URL])
return (oldETag === eTag && oldUrl === url)
}
export async function loadData (db, emojiBaseData, url, eTag) {
const transformedData = transformEmojiBaseData(emojiBaseData)
const [oldETag, oldUrl] = await get(db, STORE_META, [KEY_ETAG, KEY_URL])
if (oldETag === eTag && oldUrl === url) {
return
}
await dbPromise(db, [STORE_EMOJI, STORE_META], MODE_READWRITE, ([emojiStore, metaStore]) => {
let oldETag
let oldUrl
let oldKeys
let todo = 0
function checkFetched () {
if (++todo === 3) {
onFetched()
}
}
function onFetched () {
if (oldETag === eTag && oldUrl === url) {
// check again within the transaction to guard against concurrency, e.g. multiple browser tabs
return
}
if (oldKeys.length) {
for (const key of oldKeys) {
emojiStore.delete(key)
}
}
insertData()
}
function insertData () {
for (const data of transformedData) {
emojiStore.put(data)
}
metaStore.put(eTag, KEY_ETAG)
metaStore.put(url, KEY_URL)
}
metaStore.get(KEY_ETAG).onsuccess = e => {
oldETag = e.target.result
checkFetched()
}
metaStore.get(KEY_URL).onsuccess = e => {
oldUrl = e.target.result
checkFetched()
}
emojiStore.getAllKeys().onsuccess = e => {
oldKeys = e.target.result
checkFetched()
}
})
}
export async function getEmojiByGroup (db, group) {
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true)
emojiStore.index(INDEX_GROUP_AND_ORDER).getAll(range).onsuccess = e => {
cb(e.target.result)
}
})
}
export async function getEmojiBySearchPrefix (db, prefix) {
prefix = prefix.toLowerCase()
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.bound(prefix, prefix + '\uffff', false, true)
emojiStore.index(INDEX_TOKENS).getAll(range).onsuccess = e => {
cb(e.target.result)
}
})
}
export async function getEmojiByShortcode (db, shortcode) {
shortcode = shortcode.toLowerCase()
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.only(shortcode)
emojiStore.index(INDEX_TOKENS).getAll(range).onsuccess = e => {
// of course, we could add an extra index just for shortcodes, but it seems
// simpler to just re-use the existing tokens index and filter in-memory
const results = e.target.result.filter(emoji => emoji.shortcodes.includes(shortcode))
cb(results[0])
}
})
}
export async function getEmojiByUnicode (db, unicode) {
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
emojiStore.get(unicode).onsuccess = e => cb(e.target.result)
})
}