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();
            })));
    });
  });
});