Use WebSQL and IndexedDB in Typescript
More information about IndexedDb or WebSQL.
Let's define base interfaces for our task:
export interface IItem { id: string; value: string; } export interface IStorage<T extends IItem> { // Initial method to create storage init(name: string): Observable<IStorage<T>>; // Get the value by unique key get(key: string): Observable<T>; // Clear/remove all data in the storage clear(): Observable<T>; // Put specific value into the storage put(value: T): Observable<T>; // Get all values using the set of keys getDenseBatch(keys: string[]): Observable<T>; // Get all values from the storage all(): Observable<T>; }
Here I am using rxjs to handle results. IItem is an interface for items which we are saving, IStorage is an interface for a specific storage.
In Memory implementation
A short example how to implement mentioned interface using in-memory array:
export class MemoryStorage<T extends IItem> implements IStorage<T> { private storage: { [key: string]: T } = {}; init(name: string): Observable<MemoryStorage<T>> { return Observable.of(this); } get(key: string): Observable<T> { return Observable.of(this.storage[key]); } clear(): Observable<T> { this.storage = {}; return Observable.empty<T>(); } put(value: T): Observable<T> { if (!value.id) { value.id = Math.random().toString(36).substring(7); } this.storage[value.id] = value; return Observable.of(value); } getDenseBatch(keys: string[]): Observable<T> { return Observable.from(keys.map(x => this.storage[x])); } all(): Observable<T> { return Observable.from(Object.keys(this.storage).map(x => this.storage[x])); } }
Simple implementation of IItem:
class TestKeyValue implements IItem { public id: string; public value: string; }
Unit tests for MemoryStorage:
describe('MemoryStorage: Class', () => { let key1 = 'key1', key2 = 'key2'; let value1 = 'value1', value2 = 'value2'; function init(): MemoryStorage<TestKeyValue> { let storage = new MemoryStorage<TestKeyValue>(); storage.init('test'); return storage; } it('should create empty storage', async(() => { let storage = init(); storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeTruthy()); })); it('should save one item', async(() => { let storage = init(); storage.put({ id: key1, value: value1 }); storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeFalsy()); })); it('should save/get one item', async(() => { let storage = init(); let item = { id: key1, value: value1 }; storage.put(item); storage.get(key1).subscribe(value => expect(value).toEqual(item)); })); it('should save/get two items', async(() => { let storage = init(); let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; storage.put(items[0]); storage.put(items[1]); let i = 0; storage.getDenseBatch([key1, key2]).subscribe(value => expect(value).toEqual(items[i++])); })); it('should clear saved items', async(() => { let storage = init(); let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; storage.put(items[0]); storage.put(items[1]); storage.clear(); storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeTruthy()); })); });
WebSQL implementation
Current implementation just just for objects where key (string) is unique string value, value (string) is a payload.
export class WebSQLStorage<T extends IItem> implements IStorage<T> { private db: Database; private databaseName: string = 'TripNoteDB'; private name: string; constructor() { this.db = window.openDatabase(this.databaseName, '1.0', `Store information`, 40 * 1024 * 1024); } init(name: string): Observable<WebSQLStorage<T>> { this.name = name; return Observable.create((observer: Observer<WebSQLStorage<T>>) => { this.db.transaction( (tx) => tx.executeSql(`CREATE TABLE IF NOT EXISTS ${name} (key unique, value string)`, [], (t, results) => { observer.next(this); observer.complete(); }, (t, message) => { observer.error(message.message.toString()); return true; }) ); }); } get(key: string): Observable<T> { return Observable.create((observer: Observer<T>) => { this.db.transaction((tx) => { tx.executeSql(`SELECT * FROM ${this.name} WHERE key='${key}'`, [], (t, results) => { let len = results.rows.length; if (len === 0) { observer.next(undefined); } else if (len === 1) { observer.next(results.rows.item(0)); } else { observer.error('There should be no more than one entry'); } observer.complete(); }, (t, message) => { observer.error(message.message.toString()); return true; }); }); }); } clear() { return Observable.create((observer: Observer<T>) => { this.db.transaction((tx) => { tx.executeSql(`DELETE FROM ${this.name}`, [], (t, r) => observer.complete(), (t, e) => { observer.error(e.message.toString()); return true; }); }); }); } all(): Observable<T> { return Observable.create((observer: Observer<T>) => { this.db.transaction((tx) => { tx.executeSql(`SELECT * FROM ${this.name}`, [], (t, results) => { for (let i = 0; i < results.rows.length; i++) { observer.next(results.rows.item(i)); } observer.complete(); }, (t, message) => { observer.error(message.message.toString()); return true; }); }); }); } put(value: T): Observable<T> { return Observable.create((observer: Observer<T>) => { this.db.transaction((tx) => { tx.executeSql(`INSERT OR REPLACE INTO ${this.name} VALUES (?, ?)`, [value.id, value.value], () => { observer.next(value); observer.complete(); }, (t, e) => { observer.error(e.message.toString()); return true; }); }); }); } getDenseBatch(keys: string[]): Observable<T> { if (keys.length === 0) { return Observable.empty<T>(); }; return Observable.create((observer: Observer<T[]>) => { this.db.transaction((tx) => { let key = keys.map(x => '\'' + x + '\'').join(','); tx.executeSql(`SELECT * FROM ${this.name} WHERE key IN (${key})`, [], (t, results) => { for (let i = 0; i < results.rows.length; i++) { observer.next(results.rows.item(i)); } observer.complete(); }, (t, e) => { observer.error(e.message.toString()); return true; }); }); }); } }
describe('WebSQLStorage: Class', () => { let key1 = 'key1', key2 = 'key2'; let value1 = 'value1', value2 = 'value2'; it('should create empty storage', async(() => { let storage = new WebSQLStorage<TestKeyValue>(); storage.init('test1').subscribe(() => { storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeTruthy()); }); })); it('should save one item ', async(() => { let storage = new WebSQLStorage<TestKeyValue>(); storage.init('test2').subscribe(() => { storage.put({ id: key1, value: value1 }).subscribe(() => { storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeFalsy()); }); }); })); it('should save/get one item', async(() => { let storage = new WebSQLStorage<TestKeyValue>(); storage.init('test3').subscribe(() => { let item = { id: key1, value: value1 }; storage.put(item).subscribe(() => { storage.get(key1).subscribe(value => { expect(value.value).toEqual(item.value); }); }); }); })); it('should save/get two items', async(() => { let storage = new WebSQLStorage<TestKeyValue>(); storage.init('test4').subscribe(() => { let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; storage.put(items[0]) .subscribe(() => storage.put(items[1]) .subscribe(() => { let i = 0; storage.getDenseBatch([key1, key2]) .subscribe(value => expect(value.value).toEqual(items[i++].value)); })); }); })); it('should clear saved items', async(() => { let storage = new WebSQLStorage<TestKeyValue>(); storage.init('test5').subscribe(() => { let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; storage.put(items[0]) .zip(() => storage.put(items[1])) .subscribe(() => storage.clear() .subscribe(() => { storage.all().isEmpty().subscribe(isAny => expect(isAny).toBeTruthy()); })); }); })); });
IndexedDB implementation
How to use IndexedDB is here. There are very useful tricks.
export class IndexedDBStorage<T extends IItem> implements IStorage<T> { private databaseName: string = 'TripNoteDB'; private name: string; private getDb(version?: number, storeName?: string): Observable<IDBDatabase> { return Observable.create((observer: Observer<number>) => { let req = version && version > 0 ? window.indexedDB.open(this.databaseName, version) : window.indexedDB.open(this.databaseName); req.onsuccess = (e) => { let db = (<any>event.target).result; observer.next(db); db.close(); observer.complete(); }; req.onupgradeneeded = (e) => { let db = (<any>e.target).result; if (storeName && !db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'id' }); let transaction = (<any>e.target).transaction; transaction.oncomplete = (event) => { observer.next(db); db.close(); observer.complete(); }; }; }; req.onblocked = (event) => observer.error('IndexedDB is blocked'); req.onerror = (e) => observer.error(e.error); }); } private getVersionOfDb(name: string): Observable<number> { return this.getDb().map(db => { if (!db.objectStoreNames.contains(this.name)) { return db.version + 1; } else { return db.version; } }); } init(name: string): Observable<IndexedDBStorage<T>> { this.name = name; return Observable.create((observer: Observer<IndexedDBStorage<T>>) => { this.getVersionOfDb(name).subscribe((version) => { this.getDb(version, name).subscribe(db => { observer.next(this); observer.complete(); }); }); }); } all(): Observable<T> { return Observable.create((observer: Observer<T>) => { this.getDb().subscribe(db => { let req = db.transaction(this.name, 'readwrite').objectStore(this.name) .openCursor(); req.onsuccess = (e) => { let res = (<any>event.target).result; if (res) { observer.next(res.value); res.continue(); } observer.complete(); }; req.onerror = (e) => observer.error(e.error); }); }); } get(key: string): Observable<T> { return Observable.create((observer: Observer<T>) => { this.getDb().subscribe(db => { let req = db.transaction(this.name).objectStore(this.name).get(key); req.onerror = (e) => observer.error(e.error); req.onsuccess = (e) => { observer.next(req.result); observer.complete(); }; }); }); } clear(): Observable<IStorage<T>> { return Observable.create((observer: Observer<IStorage<T>>) => { this.getDb().subscribe(db => { let req = db.transaction(this.name, 'readwrite').objectStore(this.name).clear(); req.onerror = (e) => observer.error(e.error); req.onsuccess = (e) => { observer.next(this); observer.complete(); }; }); }); } put(value: T): Observable<T> { return Observable.create((observer: Observer<T>) => { this.getDb().subscribe(db => { let req = db.transaction(this.name, 'readwrite').objectStore(this.name).put(value); req.onerror = (e) => { observer.error(e.error); }; req.onsuccess = (e) => { observer.next(value); observer.complete(); }; }); }); } getDenseBatch(keys: string[]): Observable<T> { return Observable.create((observer: Observer<T>) => { this.getDb().subscribe(db => { let set = keys.sort(); let i = 0; let req = db.transaction(this.name).objectStore(this.name) .openCursor(); req.onsuccess = (e) => { let cursor = (<any>event.target).result; if (!cursor) { observer.complete(); return; } let key = cursor.key; while (key > set[i]) { // The cursor has passed beyond this key. Check next. ++i; if (i === set.length) { // There is no next. Stop searching. observer.complete(); return; } } if (key === set[i]) { // The current cursor value should be included and we should continue // a single step in case next item has the same key or possibly our // next key in set. observer.next(cursor.value); cursor.continue(); } else { // cursor.key not yet at set[i]. Forward cursor to the next key to hunt for. cursor.continue(set[i]); } }; req.onerror = (e) => observer.error(e.error); }); }); } }
Unit tests for indexedDB:
describe('IndexedDBStorage: Class', () => { let key1 = 'key1', key2 = 'key2'; let value1 = 'value1', value2 = 'value2'; it('should create empty storage', (done) => { let storage = new IndexedDBStorage<TestKeyValue>(); storage.init('test1').subscribe(() => { storage.all().isEmpty().subscribe(isAny => { expect(isAny).toBeTruthy(); done(); }); }); }); it('should save one item ', (done) => { let storage = new IndexedDBStorage<TestKeyValue>(); storage.init('test2').subscribe(() => { storage.put({ id: key1, value: value1 }).subscribe(() => { storage.all().isEmpty().subscribe(isAny => { expect(isAny).toBeFalsy(); done(); }); }); }); }); it('should save/get one item', (done) => { let storage = new IndexedDBStorage<TestKeyValue>(); storage.init('test3').subscribe(() => { let item = { id: key1, value: value1 }; storage.put(item).subscribe(() => { storage.get(key1).subscribe(value => { expect(value).toEqual(item); done(); }); }); }); }); it('should save/get two items', (done) => { let storage = new IndexedDBStorage<TestKeyValue>(); storage.init('test4').subscribe(() => { let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; let item1 = storage.put(items[0]) .merge(storage.put(items[1])).last() .subscribe(y => { storage.getDenseBatch([key1, key2]).toArray().subscribe(x => { expect(x[0]).toEqual(items[0]); expect(x[1]).toEqual(items[1]); done(); }); }); }); }); it('should clear saved items', (done) => { let storage = new IndexedDBStorage<TestKeyValue>(); storage.init('test5').subscribe(() => { let items = [{ id: key1, value: value1 }, { id: key2, value: value2 }]; storage.put(items[0]) .merge(storage.put(items[1])).last() .subscribe(x => storage.clear() .subscribe(y => storage.all().isEmpty().subscribe(isAny => { expect(isAny).toBeTruthy(); done(); }))); }); }); });