UNPKG

@blakewatson/datastore

Version:

Save data to IndexedDB using localStorage APIs.

315 lines (243 loc) 7.82 kB
// @ts-check const DEFAULT_DB_NAME = 'Default DB'; const DEFAULT_STORE_NAME = 'data'; /** * @typedef {Object} SetupDbOptions * @property {string} name * @property {number} [version] * @property {string[] | string} [storesToCreate] * @property {(db: IDBDatabase, stores: DataStore[]) => void} [onUpgradeNeeded] */ export default class DataStore { /** @type {string | null} */ dbName = null; /** @type {string | null} */ storeName = null; /** * Create a new database with one or more stores. * @param {SetupDbOptions} options */ static setupDb(options) { const { name, version = 1, storesToCreate = DEFAULT_STORE_NAME, onUpgradeNeeded = null, } = options; return new Promise((resolve, reject) => { const request = indexedDB.open(name, version); request.onerror = (event) => { // @ts-ignore console.error('Error opening database:', event.target.error); reject(new Error('Error opening database')); }; request.onsuccess = (event) => { resolve(request.result); }; request.onupgradeneeded = (event) => { /** @type {IDBDatabase} */ const db = request.result; const existingStores = db.objectStoreNames; const dataStoreInstances = []; if (Array.isArray(storesToCreate)) { storesToCreate.forEach((storeName) => { if (existingStores.contains(storeName)) { return; } db.createObjectStore(storeName); dataStoreInstances.push(new DataStore(name, storeName)); }); } else { if (existingStores.contains(storesToCreate)) { return; } db.createObjectStore(storesToCreate); dataStoreInstances.push(new DataStore(name, storesToCreate)); } if (onUpgradeNeeded) { onUpgradeNeeded(db, dataStoreInstances); } }; }); } /** * @param {string} dbName * @param {string} storeName */ constructor(dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME) { this.dbName = dbName; this.storeName = storeName; } getDb() { return new Promise((resolve, reject) => { // @ts-ignore we know this.dbName and this.version exist const request = indexedDB.open(this.dbName, this.version); request.onerror = (event) => { // @ts-ignore console.error('Error opening database:', event.target.error); reject(new Error('Error opening database')); }; request.onsuccess = (event) => { resolve(request.result); }; request.onupgradeneeded = (event) => { console.log('Upgrade needed'); /** @type {IDBDatabase} */ const db = request.result; const existingStores = db.objectStoreNames; // @ts-ignore we know this.storeName is a string if (!existingStores.contains(this.storeName)) { // @ts-ignore we know this.storeName is a string db.createObjectStore(this.storeName); } }; }); } /** @param {string | number} key */ async getItem(key) { /** @type {IDBDatabase} */ const db = await this.getDb(); if (typeof key === 'number') { key = key.toString(); } if (typeof key !== 'string') { throw new Error('Key must be a string'); } return new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readonly'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.get(key); request.onerror = (event) => reject(request.error); request.onsuccess = () => { resolve(request.result); db.close(); }; }); } /** * @param {string | number} key * @param {any} value */ async setItem(key, value) { /** @type {IDBDatabase} */ const db = await this.getDb(); if (typeof key === 'number') { key = key.toString(); } if (typeof key !== 'string') { throw new Error('Key must be a string'); } return /** @type {Promise<void>} */ ( new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readwrite'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.put(value, key); request.onerror = (event) => reject(request.error); request.onsuccess = () => { db.close(); resolve(); }; }) ); } /** @param {string | number} key */ async removeItem(key) { /** @type {IDBDatabase} */ const db = await this.getDb(); if (typeof key === 'number') { key = key.toString(); } if (typeof key !== 'string') { throw new Error('Key must be a string'); } return /** @type {Promise<void>} */ ( new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readwrite'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.delete(key); request.onerror = (event) => reject(request.error); request.onsuccess = () => { db.close(); resolve(); }; }) ); } async keys() { /** @type {IDBDatabase} */ const db = await this.getDb(); return new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readonly'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.getAllKeys(); request.onerror = (event) => reject(request.error); request.onsuccess = () => { resolve(request.result); db.close(); }; }); } async count() { /** @type {IDBDatabase} */ const db = await this.getDb(); return new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readonly'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.count(); request.onerror = (event) => reject(request.error); request.onsuccess = () => { resolve(request.result); db.close(); }; }); } async length() { return this.count(); } async clear() { /** @type {IDBDatabase} */ const db = await this.getDb(); return /** @type {Promise<void>} */ ( new Promise((resolve, reject) => { if (!this.storeName) { reject(new Error('Store name not set')); return; } const transaction = db.transaction(this.storeName, 'readwrite'); transaction.onerror = (event) => reject(transaction.error); const store = transaction.objectStore(this.storeName); const request = store.clear(); request.onerror = (event) => reject(request.error); request.onsuccess = () => { db.close(); resolve(); }; }) ); } }