sync-idb-kvs
Version:
Synchronous IndexedDB Key-Value Store(Requires async Initialization)
198 lines • 6.95 kB
JavaScript
import MutablePromise from "mutable-promise";
const storeName = "kvStore";
export class SyncIDBStorage {
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(dbName, initialData, opt = {}) {
const a = new AsyncIDBStorage(dbName, initialData);
const s = new SyncIDBStorage(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(asyncStorage, channelName) {
this.asyncStorage = asyncStorage;
this.channelName = channelName;
this.storageType = "idb";
//private db: IDBDatabase | null = null;
this.memoryCache = {}; // メモリキャッシュ
//uncommitedCounter=new UncommitCounter();
this.loadedAll = false;
this.passiveLoadingPromise = new MutablePromise();
}
getItem(key) {
return this.memoryCache[key] ?? null;
}
setItem(key, value) {
this.ensureLoaded();
this.memoryCache[key] = value;
//this._saveToIndexedDB(key, value);
this.asyncStorage.setItem(key, value);
}
removeItem(key) {
this.ensureLoaded();
delete this.memoryCache[key];
//this._deleteFromIndexedDB(key);
this.asyncStorage.removeItem(key);
}
itemExists(key) {
this.ensureLoaded();
return key in this.memoryCache;
}
keys() {
this.ensureLoaded();
return Object.keys(this.memoryCache)[Symbol.iterator]();
}
async reload(key) {
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(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
export class AsyncIDBStorage {
constructor(dbName = "SyncStorageDB", initialData) {
this.dbName = dbName;
this.initialData = initialData;
this.db = null;
this.uncommitedCounter = new UncommitCounter();
}
async initDB(s) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
this.loadAllData(s).then(resolve).catch(reject);
};
request.onerror = (event) => reject(event.target.error);
});
}
async loadAllData(s) {
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()),
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) {
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, value) {
return new Promise((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.error);
}).finally(() => { this.uncommitedCounter.dec(); });
}
async removeItem(key) {
return new Promise((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.error);
}).finally(() => { this.uncommitedCounter.dec(); });
}
async waitForCommit() {
return await this.uncommitedCounter.wait();
}
}
class UncommitCounter {
constructor() {
this.value = 0;
}
inc() {
this.value++;
if (!this.promise)
this.promise = new MutablePromise();
}
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;
}
}
//# sourceMappingURL=index.js.map