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'
|
2020-06-02 06:24:46 +02:00
|
|
|
import { jsonChecksum } from './utils/jsonChecksum'
|
2020-06-12 06:12:00 +02:00
|
|
|
import { closeDatabase, deleteDatabase, openDatabase } from './databaseLifecycle'
|
2020-06-04 03:09:40 +02:00
|
|
|
import {
|
|
|
|
isEmpty, hasData, loadData, 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-05 03:31:31 +02:00
|
|
|
import { log } from '../shared/log'
|
2020-06-05 04:06:51 +02:00
|
|
|
import { getETag, getETagAndData } from './utils/ajax'
|
2020-06-14 20:30:38 +02:00
|
|
|
import { customEmojiIndex } from './customEmojiIndex'
|
2020-06-05 04:06:51 +02:00
|
|
|
|
|
|
|
async function checkForUpdates (db, dataSource) {
|
|
|
|
// just do a simple HEAD request first to see if the eTags match
|
|
|
|
let emojiBaseData
|
|
|
|
let eTag = await getETag(dataSource)
|
|
|
|
if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers
|
|
|
|
const eTagAndData = await getETagAndData(dataSource)
|
|
|
|
eTag = eTagAndData[0]
|
|
|
|
emojiBaseData = eTagAndData[1]
|
|
|
|
if (!eTag) {
|
|
|
|
eTag = await jsonChecksum(emojiBaseData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (await hasData(db, dataSource, eTag)) {
|
|
|
|
log('Database already populated')
|
|
|
|
} else {
|
|
|
|
log('Database update available')
|
|
|
|
if (!emojiBaseData) {
|
|
|
|
const eTagAndData = await getETagAndData(dataSource)
|
|
|
|
emojiBaseData = eTagAndData[1]
|
|
|
|
}
|
|
|
|
await loadData(db, emojiBaseData, dataSource, eTag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadDataForFirstTime (db, dataSource) {
|
|
|
|
let [eTag, emojiBaseData] = await getETagAndData(dataSource)
|
|
|
|
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(emojiBaseData)
|
|
|
|
}
|
|
|
|
|
|
|
|
await loadData(db, emojiBaseData, dataSource, eTag)
|
|
|
|
}
|
2020-05-13 05:25:46 +02:00
|
|
|
|
2020-06-02 06:24: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-05-13 05:25:46 +02:00
|
|
|
this._dataSource = dataSource
|
|
|
|
this._locale = locale
|
2020-06-06 06:02:53 +02:00
|
|
|
this._dbName = `emoji-picker-element-${this._locale}`
|
2020-06-04 03:09:40 +02:00
|
|
|
this._db = undefined
|
2020-06-10 05:03:17 +02:00
|
|
|
this._lazyUpdate = undefined
|
2020-06-14 20:30:38 +02:00
|
|
|
this._custom = customEmojiIndex(customEmoji)
|
|
|
|
|
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)
|
|
|
|
const dataSource = this._dataSource
|
|
|
|
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
|
2020-06-10 05:03:17 +02:00
|
|
|
this._lazyUpdate = checkForUpdates(db, dataSource)
|
2020-06-05 04:06:51 +02:00
|
|
|
}
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
|
2020-06-02 06:24:46 +02:00
|
|
|
async ready () {
|
2020-06-10 05:03:17 +02:00
|
|
|
if (!this._ready) {
|
|
|
|
this._ready = this._init()
|
|
|
|
}
|
2020-06-04 03:11:24 +02:00
|
|
|
return this._ready
|
2020-06-02 06:24:46 +02:00
|
|
|
}
|
|
|
|
|
2020-05-13 05:25:46 +02:00
|
|
|
async getEmojiByGroup (group) {
|
2020-05-17 02:51:37 +02:00
|
|
|
assertNumber(group)
|
2020-06-10 05:03:17 +02:00
|
|
|
await this.ready()
|
2020-06-04 03:09:40 +02:00
|
|
|
const emojis = await getEmojiByGroup(this._db, group)
|
2020-05-17 19:56:31 +02:00
|
|
|
return uniqEmoji(emojis)
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
|
2020-06-06 02:41:38 +02:00
|
|
|
async getEmojiBySearchQuery (query) {
|
|
|
|
assertNonEmptyString(query)
|
2020-06-10 05:03:17 +02:00
|
|
|
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))
|
|
|
|
return [
|
|
|
|
...customs,
|
|
|
|
...natives
|
|
|
|
]
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async getEmojiByShortcode (shortcode) {
|
|
|
|
assertNonEmptyString(shortcode)
|
2020-06-10 05:03:17 +02:00
|
|
|
await this.ready()
|
2020-06-14 20:30:38 +02:00
|
|
|
const custom = this._custom.byShortcode(shortcode)
|
|
|
|
if (custom) {
|
|
|
|
return custom
|
|
|
|
}
|
2020-06-05 17:51:03 +02:00
|
|
|
return getEmojiByShortcode(this._db, shortcode)
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async getEmojiByUnicode (unicode) {
|
|
|
|
assertNonEmptyString(unicode)
|
2020-06-10 05:03:17 +02:00
|
|
|
await this.ready()
|
2020-06-04 03:09:40 +02:00
|
|
|
return getEmojiByUnicode(this._db, unicode)
|
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-14 20:30:38 +02:00
|
|
|
return getTopFavoriteEmoji(this._db, this._custom, limit)
|
|
|
|
}
|
|
|
|
|
|
|
|
set customEmoji (customEmojis) {
|
|
|
|
this._custom = customEmojiIndex(customEmojis)
|
|
|
|
}
|
|
|
|
|
|
|
|
get customEmoji () {
|
|
|
|
return this._custom.all
|
2020-06-12 06:12:00 +02:00
|
|
|
}
|
|
|
|
|
2020-06-15 02:30:32 +02:00
|
|
|
getCustomEmojiByName (name) {
|
|
|
|
assertNonEmptyString(name)
|
2020-06-15 02:39:52 +02:00
|
|
|
return this._custom.byName(name) || null
|
2020-06-15 02:30:32 +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-10 05:07:45 +02:00
|
|
|
if (this._db) {
|
2020-06-10 05:03:17 +02:00
|
|
|
this._db = this._ready = this._lazyUpdate = undefined
|
2020-06-10 06:44:53 +02:00
|
|
|
return true // we need to actually run the close/delete logic, so we return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async close () {
|
|
|
|
if (await this._shutdown()) {
|
2020-06-06 07:18:02 +02:00
|
|
|
await closeDatabase(this._dbName)
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete () {
|
2020-06-10 06:44:53 +02:00
|
|
|
if (await this._shutdown()) {
|
2020-06-06 07:18:02 +02:00
|
|
|
await deleteDatabase(this._dbName)
|
2020-05-13 05:25:46 +02:00
|
|
|
}
|
|
|
|
}
|
2020-05-17 02:20:00 +02:00
|
|
|
}
|