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

163 lines
4.8 KiB
JavaScript
Raw Normal View History

2020-05-13 05:25:46 +02:00
import { assertNonEmptyString } from './utils/assertNonEmptyString'
2020-05-17 02:51:37 +02:00
import { assertNumber } from './utils/assertNumber'
2020-06-12 06:12:00 +02:00
import {
DEFAULT_DATA_SOURCE,
DEFAULT_LOCALE,
KEY_PREFERRED_SKINTONE,
STORE_KEYVALUE
} from './constants'
2020-05-17 19:56:31 +02:00
import { uniqEmoji } from './utils/uniqEmoji'
import {
closeDatabase,
deleteDatabase,
2024-03-20 06:12:14 +01:00
setOnCloseListener,
2020-06-27 20:30:47 +02:00
openDatabase
} from './databaseLifecycle'
2020-06-04 03:09:40 +02:00
import {
2020-06-27 20:30:47 +02:00
isEmpty, getEmojiByGroup,
2020-06-12 06:12:00 +02:00
getEmojiBySearchQuery, getEmojiByShortcode, getEmojiByUnicode,
get, set, getTopFavoriteEmoji, incrementFavoriteEmojiCount
2020-06-04 03:09:40 +02:00
} from './idbInterface'
2020-06-14 20:30:38 +02:00
import { customEmojiIndex } from './customEmojiIndex'
import { cleanEmoji } from './utils/cleanEmoji'
2020-06-27 20:30:47 +02:00
import { loadDataForFirstTime, checkForUpdates } from './dataLoading'
2020-05-13 05:25:46 +02:00
export default class Database {
2020-06-14 20:30:38 +02:00
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
2020-06-17 09:03:50 +02:00
this.dataSource = dataSource
this.locale = locale
this._dbName = `emoji-picker-element-${this.locale}`
2020-06-04 03:09:40 +02:00
this._db = undefined
this._lazyUpdate = undefined
2020-06-14 20:30:38 +02:00
this._custom = customEmojiIndex(customEmoji)
this._clear = this._clear.bind(this)
2020-06-04 03:11:24 +02:00
this._ready = this._init()
2020-05-13 05:25:46 +02:00
}
async _init () {
2020-06-05 04:06:51 +02:00
const db = this._db = await openDatabase(this._dbName)
2024-03-20 06:12:14 +01:00
setOnCloseListener(db, this._clear)
2020-06-17 09:03:50 +02:00
const dataSource = this.dataSource
2020-06-05 04:06:51 +02:00
const empty = await isEmpty(db)
2020-05-17 02:20:00 +02:00
2020-06-05 04:06:51 +02:00
if (empty) {
await loadDataForFirstTime(db, dataSource)
} else { // offline-first - do an update asynchronously
this._lazyUpdate = checkForUpdates(db, dataSource)
2020-06-05 04:06:51 +02:00
}
2020-05-13 05:25:46 +02:00
}
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()
}
}
2020-05-13 05:25:46 +02:00
async getEmojiByGroup (group) {
2020-05-17 02:51:37 +02:00
assertNumber(group)
await this.ready()
return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)
2020-05-13 05:25:46 +02:00
}
2020-06-06 02:41:38 +02:00
async getEmojiBySearchQuery (query) {
assertNonEmptyString(query)
await this.ready()
2020-06-14 20:30:38 +02:00
const customs = this._custom.search(query)
const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji)
2020-06-14 20:30:38 +02:00
return [
...customs,
...natives
]
2020-05-13 05:25:46 +02:00
}
async getEmojiByShortcode (shortcode) {
assertNonEmptyString(shortcode)
await this.ready()
2020-06-14 20:30:38 +02:00
const custom = this._custom.byShortcode(shortcode)
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))
2020-05-13 05:25:46 +02:00
}
2020-06-15 08:38:43 +02:00
async getEmojiByUnicodeOrName (unicodeOrName) {
assertNonEmptyString(unicodeOrName)
await this.ready()
2020-06-15 08:38:43 +02:00
const custom = this._custom.byName(unicodeOrName)
if (custom) {
return custom
}
return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))
2020-05-13 05:25:46 +02:00
}
2020-06-12 05:04:42 +02:00
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)
}
2020-06-14 23:42:05 +02:00
async incrementFavoriteEmojiCount (unicodeOrName) {
assertNonEmptyString(unicodeOrName)
2020-06-12 06:12:00 +02:00
await this.ready()
2020-06-14 23:42:05 +02:00
return incrementFavoriteEmojiCount(this._db, unicodeOrName)
2020-06-12 06:12:00 +02:00
}
2020-06-14 20:30:38 +02:00
async getTopFavoriteEmoji (limit) {
assertNumber(limit)
2020-06-12 06:12:00 +02:00
await this.ready()
2020-06-27 08:16:00 +02:00
return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)
2020-06-14 20:30:38 +02:00
}
set customEmoji (customEmojis) {
this._custom = customEmojiIndex(customEmojis)
}
get customEmoji () {
return this._custom.all
2020-06-12 06:12:00 +02:00
}
2020-06-10 06:44:53 +02:00
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) */ }
}
2020-06-27 20:30:47 +02:00
// clear references to IDB, e.g. during a close event
_clear () {
console.log('_clear database', this._dbName)
2020-06-27 20:30:47 +02:00
// 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
}
2020-06-10 06:44:53 +02:00
async close () {
2021-06-27 01:23:50 +02:00
await this._shutdown()
2024-03-20 06:12:14 +01:00
await closeDatabase(this._db)
2020-05-13 05:25:46 +02:00
}
async delete () {
2021-06-27 01:23:50 +02:00
await this._shutdown()
await deleteDatabase(this._dbName)
2020-05-13 05:25:46 +02:00
}
2020-05-17 02:20:00 +02:00
}