diff --git a/index.html b/index.html
new file mode 100644
index 0000000..7236d42
--- /dev/null
+++ b/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/package.json b/package.json
index 02e4046..3e9f8fb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@ztimson/utils",
- "version": "0.25.0",
+ "version": "0.25.1",
"description": "Utility library",
"author": "Zak Timson",
"license": "MIT",
@@ -31,6 +31,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
+ "fake-indexeddb": "^6.0.1",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"ts-jest": "^29.1.2",
diff --git a/src/cache.ts b/src/cache.ts
index a79d42b..9e9f9b8 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -1,10 +1,11 @@
-import {deepCopy} from './objects.ts';
+import {Collection} from './database.ts';
+import {deepCopy, JSONSanitize} from './objects.ts';
export type CacheOptions = {
/** Delete keys automatically after x amount of seconds */
ttl?: number;
/** Storage to persist cache */
- storage?: Storage;
+ storage?: Storage | Collection;
/** Key cache will be stored under */
storageKey?: string;
/** Keep or delete cached items once expired, defaults to delete */
@@ -30,13 +31,15 @@ export class Cache {
* @param options
*/
constructor(public readonly key?: keyof T, public readonly options: CacheOptions = {}) {
- if(options.storageKey && !options.storage && typeof(Storage) !== 'undefined')
- options.storage = localStorage;
- if(options.storageKey && options.storage) {
- const stored = options.storage.getItem(options.storageKey);
- if(stored) {
- try { Object.assign(this.store, JSON.parse(stored)); }
- catch { }
+ if(options.storageKey && !options.storage && typeof(Storage) !== 'undefined') options.storage = localStorage;
+ if(options.storage) {
+ if(options.storage instanceof Collection) {
+ (async () => {
+ (await options.storage?.getAll()).forEach((v: any) => this.add(v));
+ })()
+ } else if(options.storageKey) {
+ const stored = options.storage?.getItem(options.storageKey);
+ if(stored != null) try { Object.assign(this.store, JSON.parse(stored)); } catch { }
}
}
return new Proxy(this, {
@@ -57,9 +60,14 @@ export class Cache {
return value[this.key];
}
- private save() {
- if(this.options.storageKey && this.options.storage)
- this.options.storage.setItem(this.options.storageKey, JSON.stringify(this.store));
+ private save(key: K) {
+ if(this.options.storage) {
+ if(this.options.storage instanceof Collection) {
+ this.options.storage.put(key, this.store[key]);
+ } else if(this.options.storageKey) {
+ this.options.storage.setItem(this.options.storageKey, JSONSanitize(this.store));
+ }
+ }
}
/**
@@ -111,7 +119,7 @@ export class Cache {
*/
delete(key: K): this {
delete this.store[key];
- this.save();
+ this.save(key);
return this;
}
@@ -177,10 +185,10 @@ export class Cache {
set(key: K, value: T, ttl = this.options.ttl): this {
if(this.options.expiryPolicy == 'keep') delete (value)._expired;
this.store[key] = value;
- this.save();
+ this.save(key);
if(ttl) setTimeout(() => {
this.expire(key);
- this.save();
+ this.save(key);
}, (ttl || 0) * 1000);
return this;
}
diff --git a/src/database.ts b/src/database.ts
new file mode 100644
index 0000000..78e494f
--- /dev/null
+++ b/src/database.ts
@@ -0,0 +1,60 @@
+export class Collection {
+ private database: Promise;
+
+ constructor(public readonly db: string, public readonly collection: string, public readonly version?: number, private setup?: (db: IDBDatabase, event: IDBVersionChangeEvent) => void) {
+ this.database = new Promise((resolve, reject) => {
+ const req = indexedDB.open(this.db, this.version);
+ req.onerror = () => reject(req.error);
+ req.onsuccess = () => resolve(req.result);
+ req.onupgradeneeded = (event) => {
+ const db = req.result;
+ if(!db.objectStoreNames.contains(collection)) db.createObjectStore(collection, {keyPath: undefined});
+ if (this.setup) this.setup(db, event as IDBVersionChangeEvent);
+ };
+ });
+ }
+
+ async tx(collection: string, fn: ((store: IDBObjectStore) => IDBRequest), readonly?: boolean): Promise {
+ const db = await this.database;
+ const tx = db.transaction(collection, readonly ? 'readonly' : 'readwrite');
+ const store = tx.objectStore(collection);
+ return new Promise((resolve, reject) => {
+ const request = fn(store);
+ request.onsuccess = () => resolve(request.result);
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ add(value: T, key?: K): Promise {
+ return this.tx(this.collection, store => store.add(value, key));
+ }
+
+ count(): Promise {
+ return this.tx(this.collection, store => store.count(), true);
+ }
+
+ put(key: K, value: T): Promise {
+ debugger;
+ return this.tx(this.collection, store => store.put(value, key));
+ }
+
+ getAll(): Promise {
+ return this.tx(this.collection, store => store.getAll(), true);
+ }
+
+ getAllKeys(): Promise {
+ return this.tx(this.collection, store => store.getAllKeys(), true);
+ }
+
+ get(key: K): Promise {
+ return this.tx(this.collection, store => store.get(key), true);
+ }
+
+ delete(key: K): Promise {
+ return this.tx(this.collection, store => store.delete(key));
+ }
+
+ clear(): Promise {
+ return this.tx(this.collection, store => store.clear());
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 557acab..4655739 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ export * from './aset';
export * from './cache';
export * from './color';
export * from './csv';
+export * from './database';
export * from './files';
export * from './emitter';
export * from './errors';
diff --git a/tests/indexed-db.spec.ts b/tests/indexed-db.spec.ts
new file mode 100644
index 0000000..b759274
--- /dev/null
+++ b/tests/indexed-db.spec.ts
@@ -0,0 +1,65 @@
+import {Collection} from '../src';
+import 'fake-indexeddb/auto';
+
+describe('IndexedDb and Collection', () => {
+ const data = {id: 1, name: 'Alice', age: 30, email: ''};
+ const data2 = {id: 2, name: 'Bob', age: 39, email: ''};
+ let collection = new Collection('database', 'collection');
+
+ afterEach(() => collection.clear());
+
+ test('can set and get an item', async () => {
+ await collection.put(data.id, data);
+ const result = await collection.get(data.id);
+ expect(result).toEqual(data);
+ });
+
+ test('can remove an item', async () => {
+ await collection.put(data.id, data);
+ await collection.put(data2.id, data2);
+ await collection.delete(data.id);
+ const result = await collection.get(data.id);
+ const result2 = await collection.get(data2.id);
+ expect(result).toBeUndefined();
+ expect(result2).toEqual(data2);
+ });
+
+ test('can clear all items', async () => {
+ await collection.put(data.id, data);
+ await collection.put(data2.id, data2);
+ await collection.clear();
+ const result = await collection.get(data.id);
+ const result2 = await collection.get(data2.id);
+ expect(result).toBeUndefined();
+ expect(result2).toBeUndefined();
+ });
+
+ test('getItem on missing key returns undefined', async () => {
+ const result = await collection.get(-1);
+ expect(result).toBeUndefined();
+ });
+
+ test('count returns number of items', async () => {
+ expect(await collection.count()).toBe(0);
+ await collection.put(data.id, data);
+ expect(await collection.count()).toBe(1);
+ await collection.delete(data.id);
+ expect(await collection.count()).toBe(0);
+ });
+
+ test('can get all items', async () => {
+ await collection.put(data.id, data);
+ await collection.put(data2.id, data2);
+ const allItems = await collection.getAll();
+ expect(allItems).toEqual(expect.arrayContaining([data, data2]));
+ expect(allItems.length).toBe(2);
+ });
+
+ test('can get all keys', async () => {
+ await collection.put(data.id, data);
+ await collection.put(data2.id, data2);
+ const keys = await collection.getAllKeys();
+ expect(keys).toEqual(expect.arrayContaining([data.id, data2.id]));
+ expect(keys.length).toBe(2);
+ });
+});