fix: start on refactoring database
This commit is contained in:
parent
55872ba996
commit
08307187db
|
@ -9,73 +9,60 @@ import {
|
|||
import { uniqEmoji } from './utils/uniqEmoji'
|
||||
import {
|
||||
closeDatabase,
|
||||
deleteDatabase,
|
||||
addOnCloseListener,
|
||||
openDatabase
|
||||
deleteDatabase
|
||||
} from './databaseLifecycle'
|
||||
import {
|
||||
isEmpty, getEmojiByGroup,
|
||||
getEmojiByGroup,
|
||||
getEmojiBySearchQuery, getEmojiByShortcode, getEmojiByUnicode,
|
||||
get, set, getTopFavoriteEmoji, incrementFavoriteEmojiCount
|
||||
} from './idbInterface'
|
||||
import { customEmojiIndex } from './customEmojiIndex'
|
||||
import { cleanEmoji } from './utils/cleanEmoji'
|
||||
import { loadDataForFirstTime, checkForUpdates } from './dataLoading'
|
||||
import { initDatabase } from './initDatabase.js'
|
||||
|
||||
export default class Database {
|
||||
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
|
||||
this.dataSource = dataSource
|
||||
this.locale = locale
|
||||
this._dbName = `emoji-picker-element-${this.locale}`
|
||||
this._db = undefined
|
||||
this._lazyUpdate = undefined
|
||||
this._custom = customEmojiIndex(customEmoji)
|
||||
|
||||
this._clear = this._clear.bind(this)
|
||||
this._ready = this._init()
|
||||
/* no await */ this._init()
|
||||
}
|
||||
|
||||
async _init () {
|
||||
const db = this._db = await openDatabase(this._dbName)
|
||||
|
||||
addOnCloseListener(this._dbName, this._clear)
|
||||
const dataSource = this.dataSource
|
||||
const empty = await isEmpty(db)
|
||||
|
||||
if (empty) {
|
||||
await loadDataForFirstTime(db, dataSource)
|
||||
} else { // offline-first - do an update asynchronously
|
||||
this._lazyUpdate = checkForUpdates(db, dataSource)
|
||||
if (this._controller) {
|
||||
this._controller.abort()
|
||||
}
|
||||
this._controller = new AbortController()
|
||||
this._dbPromise = initDatabase(this._dbName, this.dataSource, this._clear, this._controller.signal)
|
||||
}
|
||||
|
||||
async ready () {
|
||||
const checkReady = async () => {
|
||||
if (!this._ready) {
|
||||
this._ready = this._init()
|
||||
}
|
||||
return this._ready
|
||||
}
|
||||
await checkReady()
|
||||
// There's a possibility of a race condition where the element gets added, removed, and then added again
|
||||
// with a particular timing, which would set the _db to undefined.
|
||||
// We *could* do a while loop here, but that seems excessive and could lead to an infinite loop.
|
||||
if (!this._db) {
|
||||
await checkReady()
|
||||
await this._recreateIfNecessary()
|
||||
await this._dbPromise
|
||||
}
|
||||
|
||||
async _recreateIfNecessary () {
|
||||
if (!this._controller) {
|
||||
await this._init()
|
||||
}
|
||||
const db = await this._dbPromise
|
||||
return db
|
||||
}
|
||||
|
||||
async getEmojiByGroup (group) {
|
||||
assertNumber(group)
|
||||
await this.ready()
|
||||
return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)
|
||||
const db = await this._recreateIfNecessary()
|
||||
return uniqEmoji(await getEmojiByGroup(db, group)).map(cleanEmoji)
|
||||
}
|
||||
|
||||
async getEmojiBySearchQuery (query) {
|
||||
assertNonEmptyString(query)
|
||||
await this.ready()
|
||||
const db = await this._recreateIfNecessary()
|
||||
const customs = this._custom.search(query)
|
||||
const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji)
|
||||
const natives = uniqEmoji(await getEmojiBySearchQuery(db, query)).map(cleanEmoji)
|
||||
return [
|
||||
...customs,
|
||||
...natives
|
||||
|
@ -84,45 +71,45 @@ export default class Database {
|
|||
|
||||
async getEmojiByShortcode (shortcode) {
|
||||
assertNonEmptyString(shortcode)
|
||||
await this.ready()
|
||||
const db = await this._recreateIfNecessary()
|
||||
const custom = this._custom.byShortcode(shortcode)
|
||||
if (custom) {
|
||||
return custom
|
||||
}
|
||||
return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))
|
||||
return cleanEmoji(await getEmojiByShortcode(db, shortcode))
|
||||
}
|
||||
|
||||
async getEmojiByUnicodeOrName (unicodeOrName) {
|
||||
assertNonEmptyString(unicodeOrName)
|
||||
await this.ready()
|
||||
const db = await this._recreateIfNecessary()
|
||||
const custom = this._custom.byName(unicodeOrName)
|
||||
if (custom) {
|
||||
return custom
|
||||
}
|
||||
return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))
|
||||
return cleanEmoji(await getEmojiByUnicode(db, unicodeOrName))
|
||||
}
|
||||
|
||||
async getPreferredSkinTone () {
|
||||
await this.ready()
|
||||
return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0
|
||||
const db = await this._recreateIfNecessary()
|
||||
return (await get(db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0
|
||||
}
|
||||
|
||||
async setPreferredSkinTone (skinTone) {
|
||||
assertNumber(skinTone)
|
||||
await this.ready()
|
||||
return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone)
|
||||
const db = await this._recreateIfNecessary()
|
||||
return set(db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone)
|
||||
}
|
||||
|
||||
async incrementFavoriteEmojiCount (unicodeOrName) {
|
||||
assertNonEmptyString(unicodeOrName)
|
||||
await this.ready()
|
||||
return incrementFavoriteEmojiCount(this._db, unicodeOrName)
|
||||
const db = await this._recreateIfNecessary()
|
||||
return incrementFavoriteEmojiCount(db, unicodeOrName)
|
||||
}
|
||||
|
||||
async getTopFavoriteEmoji (limit) {
|
||||
assertNumber(limit)
|
||||
await this.ready()
|
||||
return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)
|
||||
const db = await this._recreateIfNecessary()
|
||||
return (await getTopFavoriteEmoji(db, this._custom, limit)).map(cleanEmoji)
|
||||
}
|
||||
|
||||
set customEmoji (customEmojis) {
|
||||
|
@ -133,11 +120,12 @@ export default class Database {
|
|||
return this._custom.all
|
||||
}
|
||||
|
||||
async _shutdown () {
|
||||
await this.ready() // reopen if we've already been closed/deleted
|
||||
try {
|
||||
await this._lazyUpdate // allow any lazy updates to process before closing/deleting
|
||||
} catch (err) { /* ignore network errors (offline-first) */ }
|
||||
_shutdown () {
|
||||
if (this._controller) {
|
||||
this._controller.abort()
|
||||
this._controller = undefined
|
||||
this._dbPromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// clear references to IDB, e.g. during a close event
|
||||
|
@ -147,16 +135,16 @@ export default class Database {
|
|||
// The memory leak tests prove this is unnecessary. It's because:
|
||||
// 1) IDBDatabases that can no longer fire "close" automatically have listeners GCed
|
||||
// 2) we clear the manual close listeners in databaseLifecycle.js.
|
||||
this._db = this._ready = this._lazyUpdate = undefined
|
||||
this._shutdown()
|
||||
}
|
||||
|
||||
async close () {
|
||||
await this._shutdown()
|
||||
this._shutdown()
|
||||
await closeDatabase(this._dbName)
|
||||
}
|
||||
|
||||
async delete () {
|
||||
await this._shutdown()
|
||||
this._shutdown()
|
||||
await deleteDatabase(this._dbName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,37 +2,71 @@ import { getETag, getETagAndData } from './utils/ajax'
|
|||
import { jsonChecksum } from './utils/jsonChecksum'
|
||||
import { hasData, loadData } from './idbInterface'
|
||||
|
||||
export async function checkForUpdates (db, dataSource) {
|
||||
export async function checkForUpdates (db, dataSource, signal) {
|
||||
// just do a simple HEAD request first to see if the eTags match
|
||||
let emojiData
|
||||
let eTag = await getETag(dataSource)
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers
|
||||
const eTagAndData = await getETagAndData(dataSource)
|
||||
eTag = eTagAndData[0]
|
||||
emojiData = eTagAndData[1]
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!eTag) {
|
||||
eTag = await jsonChecksum(emojiData)
|
||||
}
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await hasData(db, dataSource, eTag)) {
|
||||
console.log('Database already populated')
|
||||
} else {
|
||||
console.log('Database update available')
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!emojiData) {
|
||||
const eTagAndData = await getETagAndData(dataSource)
|
||||
emojiData = eTagAndData[1]
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadData(db, emojiData, dataSource, eTag)
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadDataForFirstTime (db, dataSource) {
|
||||
export async function loadDataForFirstTime (db, dataSource, signal) {
|
||||
let [eTag, emojiData] = await getETagAndData(dataSource)
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!eTag) {
|
||||
// Handle lack of support for ETag or Access-Control-Expose-Headers
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility
|
||||
eTag = await jsonChecksum(emojiData)
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadData(db, emojiData, dataSource, eTag)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { addOnCloseListener, openDatabase } from './databaseLifecycle.js'
|
||||
import { isEmpty } from './idbInterface.js'
|
||||
import { checkForUpdates, loadDataForFirstTime } from './dataLoading.js'
|
||||
|
||||
export async function initDatabase (dbName, dataSource, onClear, signal) {
|
||||
// signal.addEventListener('abort', () => {
|
||||
// closeDatabase(dbName)
|
||||
// })
|
||||
const db = await openDatabase(dbName)
|
||||
addOnCloseListener(dbName, onClear)
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
const empty = await isEmpty(db)
|
||||
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (empty) {
|
||||
await loadDataForFirstTime(db, dataSource, signal)
|
||||
} else { // offline-first - do an update asynchronously
|
||||
await checkForUpdates(db, dataSource, signal)
|
||||
}
|
||||
return db
|
||||
}
|
|
@ -46,7 +46,7 @@ export default class PickerElement extends HTMLElement {
|
|||
delete this[prop]
|
||||
}
|
||||
}
|
||||
this._dbFlush() // wait for a flush before creating the db, in case the user calls e.g. a setter or setAttribute
|
||||
this._dbCreate()
|
||||
}
|
||||
|
||||
connectedCallback () {
|
||||
|
@ -94,7 +94,7 @@ export default class PickerElement extends HTMLElement {
|
|||
this._cmp.$set({ [prop]: newValue })
|
||||
}
|
||||
if (['locale', 'dataSource'].includes(prop)) {
|
||||
this._dbFlush()
|
||||
this._dbCreate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,17 +102,12 @@ export default class PickerElement extends HTMLElement {
|
|||
const { locale, dataSource, database } = this._ctx
|
||||
// only create a new database if we really need to
|
||||
if (!database || database.locale !== locale || database.dataSource !== dataSource) {
|
||||
if (database) {
|
||||
database.close()
|
||||
}
|
||||
this._set('database', new Database({ locale, dataSource }))
|
||||
}
|
||||
}
|
||||
|
||||
// Update the Database in one microtask if the locale/dataSource change. We do one microtask
|
||||
// so we don't create two Databases if e.g. both the locale and the dataSource change
|
||||
_dbFlush () {
|
||||
queueMicrotask(() => (
|
||||
this._dbCreate()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const definitions = {}
|
||||
|
|
Loading…
Reference in New Issue