163 lines
4.8 KiB
JavaScript
163 lines
4.8 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,
|
|
setOnCloseListener,
|
|
openDatabase
|
|
} from './databaseLifecycle'
|
|
import {
|
|
isEmpty, getEmojiByGroup,
|
|
getEmojiBySearchQuery, getEmojiByShortcode, getEmojiByUnicode,
|
|
get, set, getTopFavoriteEmoji, incrementFavoriteEmojiCount
|
|
} from './idbInterface'
|
|
import { customEmojiIndex } from './customEmojiIndex'
|
|
import { cleanEmoji } from './utils/cleanEmoji'
|
|
import { loadDataForFirstTime, checkForUpdates } from './dataLoading'
|
|
|
|
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 () {
|
|
const db = this._db = await openDatabase(this._dbName)
|
|
|
|
setOnCloseListener(db, 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)
|
|
}
|
|
}
|
|
|
|
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 () {
|
|
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._db = this._ready = this._lazyUpdate = undefined
|
|
}
|
|
|
|
async close () {
|
|
await this._shutdown()
|
|
await closeDatabase(this._db)
|
|
}
|
|
|
|
async delete () {
|
|
await this._shutdown()
|
|
await deleteDatabase(this._dbName)
|
|
}
|
|
}
|