Created
July 5, 2023 06:34
-
-
Save MaxMonteil/fb3bb1597905de796507fe3486993b9a to your computer and use it in GitHub Desktop.
IndexedDB with multiple stores
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { openDB, IDBPDatabase } from 'idb' | |
export interface PersistenceFacade<T> { | |
exists(id: string): Promise<boolean> | |
save(key: string, value: T): Promise<void> | |
bulkSave?: (data: { key: string; val: T }[]) => Promise<void> | |
getByID(id: string): Promise<T | null> | |
getAll(): Promise<T[]> | |
getAllWithQuery(constraints: any): Promise<T[]> | |
deleteByID(id: string): Promise<void> | |
clear(): Promise<void> | |
} | |
export class IndexedDB<T> implements PersistenceFacade<T> { | |
static _version = 1 | |
static _freshStart = true | |
static _checking = false | |
static _dbCheck: Promise<void> = Promise.resolve() | |
static _stores = new Set() | |
/** | |
* When the page gets reloaded, we need to update the static _version variable, | |
* whether the db has stores or not, this ensures we try to open with the correct version. | |
* This does result in db versions starting at 1, not 0 due to that initial db open. | |
* | |
* @param dbName Name of the entire local database. | |
*/ | |
_checkDbVersion(dbName: string): Promise<void> { | |
IndexedDB._checking = true | |
return new Promise((resolve) => { | |
openDB(dbName, undefined).then((db) => { | |
IndexedDB._stores = new Set(db.objectStoreNames) | |
// version should be the number of stores +1 to account for this initial check | |
IndexedDB._version = db.objectStoreNames.length + 1 | |
db.close() // always close db | |
IndexedDB._freshStart = false | |
IndexedDB._checking = false | |
resolve() | |
}) | |
}) | |
} | |
dbName: string | |
store: string | |
options: any | |
/** | |
* @param dbName Name of the entire database. | |
* @param store Name of the IndexedDB store. | |
* @param options Name of the IndexedDB store. | |
*/ | |
private constructor(dbName: string, store: string, options = {}) { | |
/** @private */ this.dbName = dbName | |
/** @private */ this.store = store | |
/** @private */ this.options = options | |
if (IndexedDB._freshStart && !IndexedDB._checking) { | |
IndexedDB._dbCheck = this._checkDbVersion(this.dbName) | |
} | |
} | |
/** | |
* Create a new instance of IndexedDB local store. | |
* | |
* @param store Name of the IndexedDB store. | |
* @param dbName Name of the entire database. | |
* @param options Name of the IndexedDB store. | |
*/ | |
static create(store: string, dbName: string, options = {}) { | |
return new IndexedDB(dbName, store, options) | |
} | |
/** | |
* Stores are actually created here instead of in the constructor, | |
* this prevents random stores from being created right at app launch when the user might not even be logged in | |
* | |
* @param Name of the store to get. | |
*/ | |
async getStore(store = this.store) { | |
await IndexedDB._dbCheck | |
if (IndexedDB._stores.has(store)) { | |
return openDB(this.dbName, undefined) | |
} | |
IndexedDB._stores.add(store) | |
IndexedDB._version = IndexedDB._stores.size + 1 | |
return openDB(this.dbName, IndexedDB._version, { | |
upgrade: (db) => db.createObjectStore(store), | |
}) | |
} | |
/** | |
* Ensure all db functions close the db before next operation. | |
* | |
* @param func Database function to call | |
* @param args Arguments to pass to the function | |
*/ | |
async _autoclose( | |
func: 'get' | 'getAll' | 'getAllKeys' | 'put' | 'delete' | 'clear', | |
...args: Parameters<IDBPDatabase[typeof func]> | |
) { | |
const db = await this.getStore() | |
// @ts-expect-error cannot spread the arguments | |
const r = await db[func](...args) | |
// always close db | |
db.close() | |
return r | |
} | |
/** | |
* Check if an item with the given id exists. | |
* | |
* @param id ID of the item to check. | |
*/ | |
async exists(id: string): Promise<boolean> { | |
const keys = await this._autoclose('getAllKeys', this.store) | |
return keys.includes(id) ?? false | |
} | |
/** | |
* Save an item to the IndexedDB store. | |
* | |
* @param key Key by which to store the value. | |
* @param val The value to store in the database. | |
*/ | |
async save(key: string, val: T) { | |
await this._autoclose('put', this.store, val, key) | |
} | |
/** | |
* Save multiple items to the IndexedDB store. | |
* | |
* @param data An array of key values to store. | |
*/ | |
async bulkSave(data: { key: string; val: T }[]) { | |
const db = await this.getStore() | |
const tx = db.transaction(this.store, 'readwrite') | |
await Promise.all([...data.map(({ key, val }) => tx.store.put(val, key))]) | |
await tx.done | |
db.close() | |
} | |
/** | |
* Get a single item by ID. | |
* | |
* @param id ID of the item to get. | |
*/ | |
async getByID(id: string): Promise<T | null> { | |
return (await this._autoclose('get', this.store, id)) ?? null | |
} | |
/** Get all the values in the store. */ | |
async getAll(): Promise<T[]> { | |
return await this._autoclose('getAll', this.store) | |
} | |
/** Get all the values in the store. */ | |
async getAllWithQuery() { | |
return await this.getAll() | |
} | |
/** | |
* Delete an item from the database by ID. | |
* @param id ID of the item to delete. | |
*/ | |
async deleteByID(id: string) { | |
return await this._autoclose('delete', this.store, id) | |
} | |
/** Delete all the items in the store. */ | |
async clear() { | |
return await this._autoclose('clear', this.store) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Made this because I needed a way to have multiple stores in IndexedDB. Haven't looked into it since but I remember it not being possible or at least not the use case for versions. It was mostly for migrations.
Using the excellent idb, this wrapper lets you create multiple stores for various different kinds of data, similar to tables in relational databases.