Last active
December 12, 2023 20:53
-
-
Save ckimrie/63334b6ad2873bd9db7ccbbf8ccdfd53 to your computer and use it in GitHub Desktop.
Example on how to achieve RxJS observable caching and storage in Angular 2+. Ideal for storing Http requests client side for offline usage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit, OnDestroy } from '@angular/core'; | |
import {Http} from "@angular/http"; | |
import { LocalCacheService } from "./local-cache.service"; | |
@Component({ | |
selector: 'app-example', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.scss'] | |
}) | |
export class ExampleComponent implements OnInit, OnDestroy { | |
constructor(public http:Http, public cache:LocalCacheService){ | |
//Cache an observable | |
let requestObservable = this.http.get("http://example.com/path/to/api").map(res => res.json()) | |
this.cache.observable('my-cache-key', requestObservable, 300).subscribe(result => { | |
//Use result | |
console.log(result) | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Injectable} from "@angular/core"; | |
import {LocalStorageService} from "./local-storage.service"; | |
import {Observable} from "rxjs/Observable"; | |
import {isEmpty, isString, isNumber, isDate} from 'lodash'; | |
@Injectable() | |
export class LocalCacheService { | |
/** | |
* Default expiry in seconds | |
* | |
* @type {number} | |
*/ | |
defaultExpires: number = 86400; //24Hrs | |
constructor(private localstorage: LocalStorageService) {} | |
/** | |
* Cache or use result from observable | |
* | |
* If cache key does not exist or is expired, observable supplied in argument is returned and result cached | |
* | |
* @param key | |
* @param observable | |
* @param expires | |
* @returns {Observable<T>} | |
*/ | |
public observable<T>(key: string, observable: Observable<T>, expires:number = this.defaultExpires): Observable<T> { | |
//First fetch the item from localstorage (even though it may not exist) | |
return this.localstorage.getItem(key) | |
//If the cached value has expired, nullify it, otherwise pass it through | |
.map((val: CacheStorageRecord) => { | |
if(val){ | |
return (new Date(val.expires)).getTime() > Date.now() ? val : null; | |
} | |
return null; | |
}) | |
//At this point, if we encounter a null value, either it doesnt exist in the cache or it has expired. | |
//If it doesnt exist, simply return the observable that has been passed in, caching its value as it passes through | |
.flatMap((val: CacheStorageRecord | null) => { | |
if (!isEmpty(val)) { | |
return Observable.of(val.value); | |
} else { | |
return observable.flatMap((val:any) => this.value(key, val, expires)); //The result may have 'expires' explicitly set | |
} | |
}) | |
} | |
/** | |
* Cache supplied value until expiry | |
* | |
* @param key | |
* @param value | |
* @param expires | |
* @returns {Observable<T>} | |
*/ | |
value<T>(key:string, value:T, expires:number|string|Date = this.defaultExpires):Observable<T>{ | |
let _expires:Date = this.sanitizeAndGenerateDateExpiry(expires); | |
return this.localstorage.setItem(key, { | |
expires: _expires, | |
value: value | |
}).map(val => val.value); | |
} | |
/** | |
* | |
* @param key | |
* @returns {Observable<null>} | |
*/ | |
expire(key:string):Observable<null>{ | |
return this.localstorage.removeItem(key); | |
} | |
/** | |
* | |
* @param expires | |
* @returns {Date} | |
*/ | |
private sanitizeAndGenerateDateExpiry(expires:string|number|Date):Date{ | |
let expiryDate:Date = this.expiryToDate(expires); | |
//Dont allow expiry dates in the past | |
if(expiryDate.getTime() <= Date.now()){ | |
return new Date(Date.now() + this.defaultExpires); | |
} | |
return expiryDate; | |
} | |
/** | |
* | |
* @param expires | |
* @returns {Date} | |
*/ | |
private expiryToDate(expires:number|string|Date):Date{ | |
if(isNumber(expires)){ | |
return new Date(Date.now() + Math.abs(expires)*1000); | |
} | |
if(isString(expires)){ | |
return new Date(expires); | |
} | |
if(isDate(expires)){ | |
return expires; | |
} | |
return new Date(); | |
} | |
} | |
/** | |
* Cache storage record interface | |
*/ | |
interface CacheStorageRecord { | |
expires: Date, | |
value: any | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as localforage from 'localforage'; | |
import {Injectable} from "@angular/core"; | |
import {Observable} from "rxjs/Observable"; | |
@Injectable() | |
export class LocalStorageService { | |
/** | |
* | |
* @param key | |
* @param value | |
* @returns {any} | |
*/ | |
public setItem<T>(key:string, value:T):Observable<T>{ | |
return Observable.fromPromise(localforage.setItem(key, value)) | |
} | |
/** | |
* | |
* @param key | |
* @returns {any} | |
*/ | |
public getItem<T>(key:string):Observable<T>{ | |
return Observable.fromPromise(localforage.getItem(key)) | |
} | |
/** | |
* | |
* @param key | |
* @returns {any} | |
*/ | |
public removeItem(key:string):Observable<void>{ | |
return Observable.fromPromise(localforage.removeItem(key)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great!