rxidb
Version:
RxJS wrapper for IndexedDB
151 lines (130 loc) • 4.42 kB
text/typescript
import { Observable, Subject, merge, forkJoin, BehaviorSubject, of } from 'rxjs';
import { map, switchMap, mapTo, tap, filter, bufferWhen, shareReplay, take } from 'rxjs/operators';
import { RxIDB } from './rxidb-db';
import { IRxIDBStore } from './rxidb.interfaces';
import { rxifyRequest, resultFromIDBEvent } from './rxidb-utils';
export type RxIDBCursorRange = string | number | IDBKeyRange | Date | IDBArrayKey | undefined;
const UPDATE_EACH: string = '__each__';
export class RxIDBStore<Model = any> implements IRxIDBStore {
private _dataUpdate$: Subject<Model[]> = new Subject();
private _dataUpdateKey$: Subject<IDBValidKey> = new Subject();
public keyPath$ = this.tx('readwrite').pipe(
map((tx) => tx.objectStore(this.name)),
map((store) => store.keyPath)
) as Observable<string|null>;
public data$: Observable<Model[]> = merge(
this.getAll(),
this._dataUpdate$
).pipe(
shareReplay(1)
);
/**
* Stream: data change time stamp
*/
private _dataTs$: BehaviorSubject<number> = new BehaviorSubject(-1);
public dataTs$: Observable<number> = this._dataTs$.asObservable();
constructor(
public name: string,
private _db: RxIDB
) {}
public clear(): Observable<void> {
return this.tx('readwrite').pipe(
map((tx) => tx.objectStore(this.name)),
map((store) => store.clear()),
switchMap(req => rxifyRequest(req)),
switchMap(result => this._refreshDataStream(result)),
take(1),
mapTo(undefined)
);
}
public cursor(
mode: 'readonly'|'readwrite' = 'readonly',
range?: RxIDBCursorRange,
direction?: IDBCursorDirection
): Observable<IDBCursorWithValue> {
return this.tx(mode).pipe(
map(tx => tx.objectStore(this.name)),
map(store => store.openCursor(range, direction)),
switchMap(req => rxifyRequest(req)),
resultFromIDBEvent
);
}
public delete(key: IDBValidKey): Observable<void> {
return this.tx('readwrite').pipe(
map((tx) => tx.objectStore(this.name)),
map((store) => store.delete(key)),
switchMap(req => rxifyRequest(req)),
switchMap(result => this._refreshDataStream(result, key)),
take(1),
mapTo(undefined)
);
}
public reset(collection: any[]): Observable<any> {
return this.tx('readwrite').pipe(
map((tx) => tx.objectStore(this.name)),
switchMap(store => rxifyRequest(store.clear()).pipe(take(1), mapTo(store))),
map(store => {
return collection.map(value => {
return rxifyRequest(store.put(value)).pipe(
take(1),
resultFromIDBEvent
);
});
}),
switchMap((tasks: Observable<any>[]) => forkJoin(tasks)),
switchMap(result => this._refreshDataStream(result, UPDATE_EACH)),
take(1)
);
}
public get(key: IDBValidKey): Observable<any> {
return merge(of(null), this._getUpdatesFor(key)).pipe(
switchMap(() => this.tx()),
map((tx) => tx.objectStore(this.name)),
map(store => store.get(key)),
switchMap(req => rxifyRequest(req)),
resultFromIDBEvent
);
}
public getAll(): Observable<Model[]> {
let cursor$ = this.cursor();
let done$ = new Subject();
return cursor$.pipe(
tap(cursor => !!cursor ? cursor.continue() : done$.next()),
filter(cursor => !!cursor),
map(cursor => cursor.value),
bufferWhen(() => done$),
take(1)
);
}
public set(value: any, key?: IDBValidKey): Observable<any> {
return this.tx('readwrite').pipe(
map((tx) => tx.objectStore(this.name)),
map(store => store.put(value, key)),
switchMap(req => rxifyRequest(req)),
resultFromIDBEvent,
switchMap(result => this._refreshDataStream(result, key))
);
}
public tx(mode: 'readonly' | 'readwrite' = 'readonly'): Observable<IDBTransaction> {
return this._db.tx(this.name, mode);
}
/**
* Trigger update stream
*/
private _refreshDataStream<T>(result: T, key: IDBValidKey = UPDATE_EACH): Observable<T> {
return this.getAll().pipe(
take(1),
tap((data) => {
this._dataTs$.next(Date.now());
this._dataUpdate$.next(data);
this._dataUpdateKey$.next(key);
}),
mapTo(result)
);
}
private _getUpdatesFor(key: IDBValidKey): Observable<any> {
return this._dataUpdateKey$.pipe(
filter(_key => key === _key)
);
}
}