import LocalForage from 'localforage'; import { Subject } from 'rxjs'; export const Drivers = { IndexedDB: LocalForage.INDEXEDDB, LocalStorage: LocalForage.LOCALSTORAGE }; export interface StorageConfig { name?: string; version?: number; size?: number; storeName?: string; description?: string; driverOrder?: Driver[]; dbKey?: string; } export type Database = typeof LocalForage; type Driver = any; const defaultConfig = { name: '__storage', storeName: '__storageKv', dbKey: '__storageKey', driverOrder: [ Drivers.IndexedDB, Drivers.LocalStorage ] }; export class InternalStorage { #config: StorageConfig; #db!: Database; #databaseInstance: Promise; #onChange: Subject = new Subject(); public get onChange() { return this.#onChange.asObservable(); } /** * Create a new Storage instance using the order of drivers and any additional config * options to pass to LocalForage. * * Possible default driverOrder options are: ['indexeddb', 'localstorage'] and the * default is that exact ordering. */ constructor(config: StorageConfig = defaultConfig) { this.#config = { ...defaultConfig, ...config }; this.#databaseInstance = this.create().then(() => this.#db); } async create(): Promise { this.#db = LocalForage.createInstance(this.#config); await this.#db.setDriver(this.#config.driverOrder || []); return this; } /** * Define a new Driver. Must be called before * initializing the database. Example: * * await storage.defineDriver(myDriver); * await storage.create(); */ async defineDriver(driver: Driver) { return LocalForage.defineDriver(driver); } /** * Get the name of the driver being used. * * @returns Name of the driver */ get driver(): string | null { return this.#db?.driver() || null; } /** * Get the value associated with the given key. * * @param key the key to identify this value * @returns Returns a promise with the value of the given key */ async get(key: string): Promise { const db = await this.#databaseInstance; return db.getItem(key).then(res => convertDates(res as any)); } /** * Set the value for the given key. * * @param key the key to identify this value * @param value the value for this key * @returns Returns a promise that resolves when the key and value are set */ async set(key: string, value: any): Promise { const db = await this.#databaseInstance; return db.setItem(key, value).then(res => { this.#onChange.next(); return res; }); } /** * Remove any value associated with this key. * * @param key the key to identify this value * @returns Returns a promise that resolves when the value is removed */ async remove(key: string): Promise { const db = await this.#databaseInstance; return db.removeItem(key).then(res => { this.#onChange.next(); return res; }); } /** * Clear the entire key value store. WARNING: HOT! * * @returns Returns a promise that resolves when the store is cleared */ async clear(): Promise { const db = await this.#databaseInstance; const keys = await this.keys(); const p = keys.map(async key => db.removeItem(key)); await Promise.all(p); this.#onChange.next(); return Promise.resolve(); } /** * @returns Returns a promise that resolves with the number of keys stored. */ async length(): Promise { const db = await this.#databaseInstance; return db.length(); } /** * @returns Returns a promise that resolves with the keys in the store. */ async keys(): Promise { const db = await this.#databaseInstance; return db.keys(); } /** * Iterate through each key,value pair. * * @param iteratorCallback a callback of the form (value, key, iterationNumber) * @returns Returns a promise that resolves when a value is returned or the iteration has finished. */ forEach(iteratorCallback: (value: T, key: string, iterationNumber: number) => T): Promise; /** * Iterate through each key,value pair. * * @param iteratorCallback a callback of the form (value, key, iterationNumber) * @returns Returns a promise that resolves when the iteration has finished. */ forEach(iteratorCallback: (value: T, key: string, iterationNumber: number) => void): Promise; /** * Iterate through each key,value pair. * * @param iteratorCallback a callback of the form (value, key, iterationNumber) * @returns Returns a promise that resolves when a value is returned or the iteration has finished. */ async forEach(iteratorCallback: (value: T, key: string, iterationNumber: number) => any): Promise { const db = await this.#databaseInstance; return db.iterate(iteratorCallback); } } const dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)$/; const utcDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/; const convertDates = (object: T): T => { if (!object || !(object instanceof Object)) return object; if (object instanceof Array) for (const item of object) convertDates(item); for (const key of Object.keys(object)) { const value = (object as any)[key]; if (value instanceof Array) for (const item of value) convertDates(item); if (value instanceof Object) convertDates(value); if (typeof value === 'string' && utcDateRegex.test(value)) (object as any)[key] = new Date(value); if (typeof value === 'string' && dateRegex.test(value)) (object as any)[key] = new Date(value); } return object; };