Skip to content

Commit

Permalink
Adding indexedDB support to saddlebag
Browse files Browse the repository at this point in the history
now we get stateful bags!

Signed-off-by: quobix <[email protected]>
  • Loading branch information
daveshanley committed Feb 23, 2024
1 parent 58724b2 commit c3785f1
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 183 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"i": "^0.3.7",
"typescript": "^5.0.2",
"vite": "^4.3.2",
"vitest": "^0.31.1"
"vitest": "^0.31.1",
"fake-indexeddb": "^5.0.2"
},
"files": [
"dist"
Expand Down
93 changes: 84 additions & 9 deletions src/bag.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {CreateBag} from "./saddlebag_engine.ts";
import {Bag} from "./saddlebag.ts";


export const BAG_OBJECT_STORE = 'bags';
export const BAG_DB_NAME = 'saddlebag';

/**
* BagManager is a singleton that manages all the bags in the application.
*/
Expand All @@ -25,31 +28,103 @@ export interface BagManager {
* Reset all bags to their initial state.
*/
resetBags(): void;

/**
* Load all stateful bags from IndexedDB.
* @return {Promise<BagDB>} a promise that resolves when all bags are loaded
*/
loadStatefulBags(): Promise<BagDB>;

/**
* Get the indexedDB database
* @return {IDBDatabase | null} the indexedDB database
*/
get db(): IDBDatabase | null;

}

interface BagDB {
db: IDBDatabase | undefined;
}

class saddlebagManager implements BagManager {
private _bags: Map<string, Bag<any>>;
private readonly _stateful: boolean;
private _db: IDBDatabase | undefined

constructor() {
constructor(stateful: boolean) {
this._bags = new Map<string, Bag<any>>();
this._stateful = stateful;
}

loadStatefulBags(): Promise<BagDB> {
return new Promise<BagDB>((resolve, reject) => {
const request = indexedDB.open(BAG_DB_NAME, 1);
request.onupgradeneeded = () => {
// @ts-ignore
this._db = request.result
this._db.createObjectStore(BAG_OBJECT_STORE);
};

request.onerror = (event) => {
reject(event);
}

request.onsuccess = () => {
// @ts-ignore
this._db = request.result

if (this._db) {
const tx = this._db.transaction(BAG_OBJECT_STORE)
const cursor = tx.objectStore(BAG_OBJECT_STORE).openCursor()

cursor.onerror = (event) => {
reject(event);
}

cursor.onsuccess = (event) => {
// @ts-ignore
let cursor = event.target.result;
if (cursor) {
let key = cursor.primaryKey;
let value = cursor.value;
const bag = this.createBag(key);
bag.populate(value);
this._bags.set(key, bag);
cursor.continue();
} else {
resolve({db: this._db});
}
}
}
}
})
}

get db(): IDBDatabase | null {
if (this._db) {
return this._db;
}
return null;
}

createBag<T>(key: string): Bag<T> {
const store: Bag<T> = CreateBag<T>();
this._bags.set(key, store);
return store;
const bag: Bag<T> = CreateBag<T>(key, this._stateful);
bag.db = this._db;
this._bags.set(key, bag);
return bag;
}

getBag<T>(key: string): Bag<T> | undefined {
if (this._bags.has(key)) {
return this._bags.get(key);
}
return CreateBag<T>();
return CreateBag<T>(key, );
}

resetBags() {
this._bags.forEach((store: Bag<any>) => {
store.reset();
this._bags.forEach((bag: Bag<any>) => {
bag.reset();
});
}
}
Expand All @@ -60,9 +135,9 @@ let _bagManagerSingleton: BagManager;
* CreateBagManager creates a singleton BagManager.
* @returns {BagManager} the singleton BagManager
*/
export function CreateBagManager(): BagManager {
export function CreateBagManager(stateful?: boolean): BagManager {
if (!_bagManagerSingleton) {
_bagManagerSingleton = new saddlebagManager();
_bagManagerSingleton = new saddlebagManager(stateful || false);
}
return _bagManagerSingleton;
}
Expand Down
10 changes: 10 additions & 0 deletions src/saddlebag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,15 @@ export interface Bag<T = any> {
* and popular.
*/
reset(): void;

/**
* id is the unique identifier of the bag.
*/
get id(): string;

/**
* db is the IndexedID database that the bag is associated with.
*/
set db(db: IDBDatabase | undefined);
}

66 changes: 59 additions & 7 deletions src/saddlebag_engine.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,61 @@
import {describe, expect, it } from 'vitest'
import {CreateBag} from "./saddlebag_engine";
import {BAG_OBJECT_STORE, CreateBagManager} from "./bag.manager";

import indexeddb from 'fake-indexeddb';

// @ts-ignore
globalThis.indexedDB = indexeddb;

describe('store basics', () => {

it('create a new stateful bag and add an item to it, then check its still there', async (): Promise<void> => {

const bm = CreateBagManager(true)
expect(bm).toBeDefined();
expect(bm.db).toBeNull();

const p = new Promise<void>((resolve) => {

bm.loadStatefulBags().then(() => {

expect(bm.db).toBeDefined();

const bag = bm.createBag<string>('foo');
if (bag) {
expect(bag.id).toEqual('foo');
expect(bag.get('foo')).toBeUndefined();
bag.set('foo', 'bar');
expect(bag.get('foo')).toEqual('bar');
}

bm.db.transaction([BAG_OBJECT_STORE])
.objectStore(BAG_OBJECT_STORE).get('foo').onsuccess = (event: any) => {

const result = event.target.result;
expect(result).toBeDefined();
expect(result.get('foo')).toEqual('bar');

// create a new bag manager and then try again, should be the same result
const bm2 = CreateBagManager(true)
expect(bm2).toBeDefined();

bm2.loadStatefulBags().then(() => {
const bag2 = bm2.getBag<string>('foo');
if (bag2) {
expect(bag2.get('foo')).toEqual('bar');
resolve(result)
}
})
}
})
})
return p
}, 500)


it('create a new bag and add an item to it', () => {
const bag = CreateBag<string>();
const bag = CreateBag<string>('boo');
expect(bag).toBeDefined();
expect(bag).not.toBeNull();
expect(bag.get('foo')).toBeUndefined();
Expand All @@ -13,10 +64,11 @@ describe('store basics', () => {
})

it('subscribe and unsubscribe to a store', () => {
const bag = CreateBag<string>();
const bag = CreateBag<string>('foo');

let counter = 0;

// @ts-ignore
const sub1 = bag.subscribe('foo', (value: string) => {
counter++;
})
Expand All @@ -41,7 +93,7 @@ describe('store basics', () => {
});

it('subscribe and unsubscribe to a store for all changes', () => {
const bag = CreateBag<string>();
const bag = CreateBag<string>('foo');

let counter = 0;

Expand All @@ -64,7 +116,7 @@ describe('store basics', () => {
});

it('subscribe and unsubscribe to a store on population', () => {
const bag = CreateBag<string>();
const bag = CreateBag<string>('foo');

let counter = 0;

Expand Down Expand Up @@ -92,7 +144,7 @@ describe('store basics', () => {
});

it('reset a bag', () => {
const bag = CreateBag<string>();
const bag = CreateBag<string>('foo');

let counter = 0;

Expand All @@ -108,7 +160,7 @@ describe('store basics', () => {
});

it('check a get value cannot be mutated', () => {
const bag = CreateBag();
const bag = CreateBag('foo');
const bar = { sleepy: 'time' };

bag.set('k', bar);
Expand All @@ -120,7 +172,7 @@ describe('store basics', () => {


it('check population cannot be mutated after storing', () => {
const bag = CreateBag();
const bag = CreateBag('foo');
const bar = { sleepy: 'time' };


Expand Down
28 changes: 25 additions & 3 deletions src/saddlebag_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
BagPopulatedSubscriptionFunction,
BagValueSubscriptionFunction, Subscription
} from "./saddlebag.ts";
import {BAG_OBJECT_STORE} from "./bag.manager.ts";

export function CreateBag<T>(): Bag<T> {
return new bag<T>();
export function CreateBag<T>(id: string, stateful?: boolean): Bag<T> {
return new bag<T>(id, stateful);
}

export class BagSubscription<T> {
Expand Down Expand Up @@ -65,21 +66,42 @@ export class BagSubscription<T> {
}

class bag<T> {
private _id: string;
private _stateful: boolean;
private _values: Map<string, T>;
private _db: IDBDatabase | undefined;
private _bagStore: IDBObjectStore | undefined;

_subscriptions: Map<string, BagValueSubscriptionFunction<T>[]>;
_allChangesSubscriptions: BagAllChangeSubscriptionFunction<T>[];
_storePopulatedSubscriptions: BagPopulatedSubscriptionFunction<T>[];

constructor() {
constructor(id: string, stateful?: boolean) {
this._values = new Map<string, T>();
this._subscriptions = new Map<string, BagValueSubscriptionFunction<T>[]>()
this._allChangesSubscriptions = [];
this._storePopulatedSubscriptions = [];
this._stateful = stateful || false;
this._id = id;
}

set(key: string, value: T): void {
this._values.set(key, structuredClone(value));
this.alertSubscribers(key, value)

if (this._stateful && this._db) {
this._db.transaction([BAG_OBJECT_STORE], 'readwrite')
.objectStore(BAG_OBJECT_STORE)
.put(this._values, this._id);
}
}

get id(): string {
return this._id;
}

set db(db: IDBDatabase | undefined) {
this._db = db;
}

reset(): void {
Expand Down
Loading

0 comments on commit c3785f1

Please sign in to comment.