emoji-picker-element/src/database/Database.js

170 lines
5.1 KiB
JavaScript

import { assertNonEmptyString } from './utils/assertNonEmptyString'
import { assertNumber } from './utils/assertNumber'
import {
DEFAULT_DATA_SOURCE,
DEFAULT_LOCALE,
KEY_PREFERRED_SKINTONE,
STORE_KEYVALUE
} from './constants'
import { uniqEmoji } from './utils/uniqEmoji'
import {
closeDatabase,
deleteDatabase
} from './databaseLifecycle'
import {
getEmojiByGroup,
getEmojiBySearchQuery, getEmojiByShortcode, getEmojiByUnicode,
get, set, getTopFavoriteEmoji, incrementFavoriteEmojiCount
} from './idbInterface'
import { customEmojiIndex } from './customEmojiIndex'
import { cleanEmoji } from './utils/cleanEmoji'
import { initializeDatabase } from './initializeDatabase.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()
}
async _init () {
try {
this._controller = new AbortController() // used to cancel inflight requests if necessary
const [db, lazyUpdate] = await initializeDatabase(
this._dbName,
this.dataSource,
this._clear,
this._controller.signal
)
this._db = db
this._lazyUpdate = lazyUpdate
} catch (err) {
if (err.name !== 'AbortError' && err.name !== 'InvalidStateError') {
throw err
}
console.info('aborted', this._dbName, this.dataSource, err.name)
// Ignore AbortErrors - we were canceled. Ignore InvalidStateErrors thrown by IDB due to database closing.
}
}
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()
}
}
async getEmojiByGroup (group) {
assertNumber(group)
await this.ready()
return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)
}
async getEmojiBySearchQuery (query) {
assertNonEmptyString(query)
await this.ready()
const customs = this._custom.search(query)
const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji)
return [
...customs,
...natives
]
}
async getEmojiByShortcode (shortcode) {
assertNonEmptyString(shortcode)
await this.ready()
const custom = this._custom.byShortcode(shortcode)
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))
}
async getEmojiByUnicodeOrName (unicodeOrName) {
assertNonEmptyString(unicodeOrName)
await this.ready()
const custom = this._custom.byName(unicodeOrName)
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))
}
async getPreferredSkinTone () {
await this.ready()
return (await get(this._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)
}
async incrementFavoriteEmojiCount (unicodeOrName) {
assertNonEmptyString(unicodeOrName)
await this.ready()
return incrementFavoriteEmojiCount(this._db, unicodeOrName)
}
async getTopFavoriteEmoji (limit) {
assertNumber(limit)
await this.ready()
return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)
}
set customEmoji (customEmojis) {
this._custom = customEmojiIndex(customEmojis)
}
get customEmoji () {
return this._custom.all
}
async _shutdown () {
if (this._controller) {
this._controller.abort()
}
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) */ }
}
// clear references to IDB, e.g. during a close event
_clear () {
console.log('_clear database', this._dbName)
// We don't need to call removeEventListener or remove the manual "close" listeners.
// 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._controller = this._db = this._ready = this._lazyUpdate = undefined
}
async close () {
await this._shutdown()
await closeDatabase(this._dbName)
}
async delete () {
await this._shutdown()
await deleteDatabase(this._dbName)
}
}