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

108 lines
3.6 KiB
JavaScript

import { initialMigration } from './migrations'
import { DB_VERSION_INITIAL, DB_VERSION_CURRENT } from './constants'
export const openIndexedDBRequests = {}
const databaseCache = {}
const onCloseListeners = {}
function handleOpenOrDeleteReq (resolve, reject, req) {
// These things are almost impossible to test with fakeIndexedDB sadly
/* istanbul ignore next */
req.onerror = () => reject(req.error)
/* istanbul ignore next */
req.onblocked = () => reject(new Error('IDB blocked'))
req.onsuccess = () => resolve(req.result)
}
async function createDatabase (dbName) {
performance.mark('createDatabase')
const db = await new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, DB_VERSION_CURRENT)
openIndexedDBRequests[dbName] = req
req.onupgradeneeded = e => {
// Technically there is only one version, so we don't need this `if` check
// But if an old version of the JS is in another browser tab
// and it gets upgraded in the future and we have a new DB version, well...
// better safe than sorry.
/* istanbul ignore else */
if (e.oldVersion < DB_VERSION_INITIAL) {
initialMigration(req.result)
}
}
handleOpenOrDeleteReq(resolve, reject, req)
})
// Handle abnormal closes, e.g. "delete database" in chrome dev tools.
// No need for removeEventListener, because once the DB can no longer
// fire "close" events, it will auto-GC.
// Unfortunately cannot test in fakeIndexedDB: https://github.com/dumbmatter/fakeIndexedDB/issues/50
/* istanbul ignore next */
db.onclose = () => closeDatabase(dbName)
performance.measure('createDatabase', 'createDatabase')
return db
}
export function openDatabase (dbName) {
if (!databaseCache[dbName]) {
databaseCache[dbName] = createDatabase(dbName)
}
return databaseCache[dbName]
}
export function dbPromise (db, storeName, readOnlyOrReadWrite, cb) {
return new Promise((resolve, reject) => {
// Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone
// are really irreplaceable data. IndexedDB is just a cache in this case.
const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' })
const store = typeof storeName === 'string'
? txn.objectStore(storeName)
: storeName.map(name => txn.objectStore(name))
let res
cb(store, txn, (result) => {
res = result
})
txn.oncomplete = () => resolve(res)
/* istanbul ignore next */
txn.onerror = () => reject(txn.error)
})
}
export function closeDatabase (dbName) {
// close any open requests
const req = openIndexedDBRequests[dbName]
const db = req && req.result
if (db) {
db.close()
const listeners = onCloseListeners[dbName]
/* istanbul ignore else */
if (listeners) {
for (const listener of listeners) {
listener()
}
}
}
delete openIndexedDBRequests[dbName]
delete databaseCache[dbName]
delete onCloseListeners[dbName]
}
export function deleteDatabase (dbName) {
return new Promise((resolve, reject) => {
// close any open requests
closeDatabase(dbName)
const req = indexedDB.deleteDatabase(dbName)
handleOpenOrDeleteReq(resolve, reject, req)
})
}
// The "close" event occurs during an abnormal shutdown, e.g. a user clearing their browser data.
// However, it doesn't occur with the normal "close" event, so we handle that separately.
// https://www.w3.org/TR/IndexedDB/#close-a-database-connection
export function addOnCloseListener (dbName, listener) {
let listeners = onCloseListeners[dbName]
if (!listeners) {
listeners = onCloseListeners[dbName] = []
}
listeners.push(listener)
}