Skip to content

Instantly share code, notes, and snippets.

@bdombro
Created November 18, 2020 16:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdombro/31563cebd5186198654bef62585af0cc to your computer and use it in GitHub Desktop.
Save bdombro/31563cebd5186198654bef62585af0cc to your computer and use it in GitHub Desktop.
KeyValueStore.ts - Class to make Indexed Database's easy for Key-Value Stores with maxAge feature
/**
* Class to make Indexed Database's easy for Key-Value Stores
*
* In addition to wrapping IDB, it also adds a feature for maxAge,
* which can discard records if createdAt + maxAge > now
*
* Adapted from https://github.com/elias551/simple-kvs
* - Adds timestamp field and garbage collection feature.
*/
export default class KeyValueStore {
config: Config = {
dbName: "kv",
tableName: "default",
maxAge: -1,
}
store: IDBObjectStore | undefined;
constructor(config: Partial<Config>) {
this.config = {...this.config, ...config};
this.getStore().then(() => {
if (this.config.maxAge > 0) setInterval(this.clearExpired, 60000);
})
}
public async get <T>(key: string) {
const store = await this.getStore()
return new Promise<T | undefined>((resolve, reject) => {
const request = store.get(key);
request.onerror = reject;
request.onsuccess = function() {
let result = this.result?.v;
resolve(typeof result !== "undefined" ? (result as T) : undefined);
};
})
}
public async getKeys () {
const store = await this.getStore()
return new Promise((resolve, reject) => {
const request = store.getAllKeys();
request.onerror = reject;
request.onsuccess = function() {
const keys = this.result.map((v) => v.toString());
resolve(keys);
};
})
}
public async set (key: string, value: any) {
const store = await this.getStore()
return new Promise<void>((resolve, reject) => {
const request = store.put({ k: key, v: value, t: Date.now() });
request.onsuccess = () => resolve();
request.onerror = reject;
})
}
public async remove (key: string) {
const store = await this.getStore()
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = reject;
})
}
public async clear () {
const store = await this.getStore()
new Promise((resolve, reject) => {
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = reject;
})
}
public async getStore(): Promise<IDBObjectStore> {
if (!this.store) {
const { dbName, tableName, maxAge } = this.config
const db = await getKeyValueDb(dbName, tableName);
const store = db.transaction(tableName, "readwrite").objectStore(tableName);
this.store = store
}
return this.store;
function getKeyValueDb(dbName: string, tableName: string) {
return new Promise<IDBDatabase>((resolve, reject) => {
const indexedDB = (window &&
(window.indexedDB ||
(window as any).mozIndexedDB ||
(window as any).webkitIndexedDB ||
(window as any).msIndexedDB));
if (!indexedDB) {
reject("indexedDB not supported");
return;
}
const request = indexedDB.open(dbName, 1);
request.onsuccess = function() {
resolve(this.result);
};
request.onerror = function(event: any) {
reject("indexedDB request error");
console.error(event);
};
request.onupgradeneeded = function() {
const store = this.result.createObjectStore(tableName, {
keyPath: "k",
});
store.createIndex("t", "t", { unique: false });
store.transaction.oncomplete = function() {
resolve(this.db);
};
};
});
}
}
public async clearExpired () {
const { maxAge } = this.config;
const store = await this.getStore();
return new Promise((resolve, reject) => {
const request = store
.index("t")
.openCursor(IDBKeyRange.upperBound(Date.now() - maxAge!));
request.onsuccess = (event) => {
// @ts-ignore
const cursor = event.target!.result;
if (cursor) {
store.delete(cursor.primaryKey);
cursor.continue();
} else {
resolve();
}
};
request.onerror = reject;
});
}
}
interface Config {
dbName: string,
tableName: string,
maxAge: number
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment