UNPKG

sync-idb-kvs

Version:

Synchronous IndexedDB Key-Value Store(Requires async Initialization)

210 lines (207 loc) 7.76 kB
import MutablePromise from "mutable-promise"; export interface IStorage<T> { storageType: string; setItem(key: string, value: T): void; getItem(key: string): T | null; removeItem(key: string): void; itemExists(key: string): boolean; keys(): IterableIterator<string>; reload(key:string):Promise<T|null>; waitForCommit():Promise<void>; } export type SyncIDBStorageOptions={ // 0 wait for loadall on mount // 1 start loadall but not wait on mount // 2 postpone loadall until first access lazy?:0|1|2, }; const storeName="kvStore"; export class SyncIDBStorage<T> implements IStorage<T> { storageType="idb"; //private db: IDBDatabase | null = null; memoryCache: Record<string, T> = {}; // メモリキャッシュ //uncommitedCounter=new UncommitCounter(); loadedAll=false; loadingPromise?:Promise<void>; passiveLoadingPromise=new MutablePromise(); getLoadingPromise(passive=false){ if(this.loadedAll)return Promise.resolve(); if(passive)return this.passiveLoadingPromise; this.loadingPromise=this.loadingPromise|| this.asyncStorage.initDB(this).then( ()=>{ this.loadedAll=true; this.passiveLoadingPromise.resolve(void 0); } ); return this.loadingPromise; } static async create<T>(dbName:string, initialData:Record<string,T>, opt={} as SyncIDBStorageOptions): Promise<SyncIDBStorage<T>> { const a=new AsyncIDBStorage<T>(dbName, initialData); const s=new SyncIDBStorage<T>(a,dbName); opt.lazy=opt.lazy||0; if(opt.lazy<2)s.getLoadingPromise(); if(!opt.lazy)await s.getLoadingPromise(); return s; } ensureLoaded(){ if(this.loadedAll)return ; throw Object.assign( new Error(`${this.channelName}: Now loading. Try again later.`), {retryPromise:this.getLoadingPromise(),} ); } constructor( public asyncStorage:AsyncIDBStorage<T>, public channelName:string, ) {} getItem(key: string): T | null { return this.memoryCache[key] ?? null; } setItem(key: string, value: T): void { this.ensureLoaded(); this.memoryCache[key] = value; //this._saveToIndexedDB(key, value); this.asyncStorage.setItem(key,value); } removeItem(key: string): void { this.ensureLoaded(); delete this.memoryCache[key]; //this._deleteFromIndexedDB(key); this.asyncStorage.removeItem(key); } itemExists(key: string): boolean { this.ensureLoaded(); return key in this.memoryCache; } keys(): IterableIterator<string> { this.ensureLoaded(); return Object.keys(this.memoryCache)[Symbol.iterator](); } async reload(key: string): Promise<T|null> { await this.getLoadingPromise(); //const value=await this._getFromIndexedDB(key); const value=await this.asyncStorage.getItem(key); if (value){ if (value!==this.memoryCache[key]){ this.memoryCache[key]=value; } } else { if (key in this.memoryCache) { delete this.memoryCache[key]; } } return value; } async waitForCommit(){ return await this.asyncStorage.uncommitedCounter.wait(); } } export function idbReqPromise<T>(request:IDBRequest<T>){ return new Promise<T>((resolve,reject)=>{ request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } export class AsyncIDBStorage<T> { private db: IDBDatabase | null = null; uncommitedCounter=new UncommitCounter(); constructor( public dbName = "SyncStorageDB", public initialData:Record<string,T>, ) {} async initDB(s:SyncIDBStorage<T>): Promise<void> { return new Promise<void>((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (event: IDBVersionChangeEvent) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName); } }; request.onsuccess = (event: Event) => { this.db = (event.target as IDBOpenDBRequest).result; this.loadAllData(s).then(resolve).catch(reject); }; request.onerror = (event: Event) => reject((event.target as IDBOpenDBRequest).error); }); } async loadAllData(s: SyncIDBStorage<T>): Promise<void> { const transaction = this.db!.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); // Get all keys and values in the same transaction const [keys,values]= await Promise.all([ idbReqPromise(store.getAllKeys()) as Promise<string[]>, idbReqPromise(store.getAll()) ]); // Both arrays have the same order keys.forEach((key, i) => { if (!(key in s.memoryCache)) { s.memoryCache[key] = values[i] ?? ""; } }); for (let key in this.initialData) { if (!(key in s.memoryCache)){ s.memoryCache[key] = this.initialData[key]; } } } async getItem(key: string): Promise<T | null> { return new Promise((resolve, reject) => { if (!this.db) return resolve(null); const transaction = this.db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = () => resolve(request.result ?? null); request.onerror = () => reject(request.error); }); } async setItem(key: string, value: T): Promise<void> { return new Promise<void>((resolve, reject) => { this.uncommitedCounter.inc(); if (!this.db) return resolve(); const transaction = this.db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); const request = store.put(value, key); request.onsuccess = () => resolve(); request.onerror = (event) => reject((event.target as IDBRequest).error); }).finally(()=>{this.uncommitedCounter.dec();}); } async removeItem(key: string): Promise<void> { return new Promise<void>((resolve, reject) => { if (!this.db) return resolve(); this.uncommitedCounter.inc(); const transaction = this.db.transaction(storeName, "readwrite"); const store = transaction.objectStore(storeName); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = (event) => reject((event.target as IDBRequest).error); }).finally(()=>{this.uncommitedCounter.dec();}); } async waitForCommit(){ return await this.uncommitedCounter.wait(); } } class UncommitCounter { private value=0; private promise: MutablePromise<void>|undefined; inc() { this.value++; if (!this.promise) this.promise=new MutablePromise<void>(); } dec() { this.value--; if (this.value<0) throw new Error("UncommitCounter: Invalid counter state."); if (this.value==0) { if (!this.promise) throw new Error("UncommitCounter: Invalid promise state."); this.promise.resolve(); delete this.promise; } } async wait(){ if (!this.promise) return; await this.promise; } }