-
-
Save ckimrie/63334b6ad2873bd9db7ccbbf8ccdfd53 to your computer and use it in GitHub Desktop.
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) | |
}); | |
} | |
} |
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 | |
} |
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)) | |
} | |
} |
+1 for packaging it...
Have you already packaged it?
Hello,
I have been using this solution for a month now. Today I received this exception. My Chrome is now v 61.
I believe this has to do with the Local Forage.
Any clue? Thanks
var b = number % 10,
output = (toInt(number % 100 / 10) === 1) ? 'th' :...<omitted>... } could not be cloned.
at http://localhost:4200/vendor.bundle.js:10127:29
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:4200/polyfills.bundle.js:7073:26)
at Object.onInvoke (http://localhost:4200/vendor.bundle.js:136945:33)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (http://localhost:4200/polyfills.bundle.js:7072:32)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (http://localhost:4200/polyfills.bundle.js:6833:43)
at http://localhost:4200/polyfills.bundle.js:7466:57
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (http://localhost:4200/polyfills.bundle.js:7106:31)
at Object.onInvokeTask (http://localhost:4200/vendor.bundle.js:136936:33)
at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (http://localhost:4200/polyfills.bundle.js:7105:36)
at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask (http://localhost:4200/polyfills.bundle.js:6873:47)
at drainMicroTaskQueue (http://localhost:4200/polyfills.bundle.js:7299:35)
at XMLHttpRequest.ZoneTask.invoke (http://localhost:4200/polyfills.bundle.js:7172:25)````
I am receiving the following error:
ERROR TypeError: this.localstorage.getItem(...).map(...).flatMap is not a function at LocalCacheService.observable (local-cache.service.ts:61)
Any help would be much appreciated! Great work
thanks for sharing
i pretty the code so please take a look and update if you like to
+1 for packaging.
Great stuff!
Hello,
Could you, please, explain how to update cache value from received data of PUT request.
I need to do thing like that:
this.cache.observable('app', this.appProvider.retrieveApp(), 8468)
.subscribe(app => {
this.app = app;
});
this.cache.observable('app', this.appProvider.updateApp(appForm), 8468)
.subscribe(app => {
this.app = app;
});
Thank you!
Hello,
Could you please support RXJS v6 with error:
TS2345: Argument of type 'OperatorFunction<CacheStorageRecord, CacheStorageRecord | null>' is not assignable to parameter of type 'OperatorFunction<{}, CacheStorageRecord | null>'. Type '{}' is not assignable to type 'CacheStorageRecord'. Property 'expires' is missing in type '{}'.
public observable(key: string, observable: Subscription, expires: number = this.defaultExpires): Observable {
return this.localstorage.getItem(key).pipe(
map((val: CacheStorageRecord) => {
if(val){
return (new Date(val.expires)).getTime() > Date.now() ? val : null
}
return null
}),
flatMap((val: CacheStorageRecord | null) => {
if (!isEmpty(val)) {
return of(val.value)
} else {
return flatMap((data:any) => this.value(key, data, expires)) //The result may have 'expires' explicitly set
}
}))
}
Thank you
Hi there, please advise what's "import * as localforage from 'localforage';" in the local-storage.service.ts? Is it a typo?
It's the localforage package, which this relies on as a dependency.
Hi,
For RXJS v6:
import {of as _observableOf} from 'rxjs';
import { mergeMap as _observableMergeMap, catchError as _observableCatch } from 'rxjs/operators';
...
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
.pipe(_observableMergeMap((val: CacheStorageRecord | null) => {
if (!isEmpty(val)) {
return _observableOf(val.value);
} else {
return observable.pipe(_observableMergeMap((val:any) => this.value(key, val, expires))); //The result may have 'expires' explicitly set
}
}))
}
Hello, I share the updated code with the current version of rxjs
A small change was made, the storage was placed in a variable, so we can change it if it is a session or local storage
localStorage | sessionStorage
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class LocalStorageService {
storage = localStorage;
/**
*
* @param key
* @param value
* @returns {any}
*/
public setItem<T>(key: string, value: T): Observable<T> {
this.storage.setItem(key, JSON.stringify(value));
return of<T>(value);
}
/**
*
* @param key
* @returns {any}
*/
public getItem<T>(key: string): Observable<T> {
const value = this.storage.getItem(key);
return of<T>(
value ? JSON.parse(this.storage.getItem(key)) : (null as T)
);
}
/**
*
* @param key
* @returns {any}
*/
public removeItem(key: string): Observable<void> {
return of(this.storage.removeItem(key));
}
}
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { isString, isNumber, isDate } from 'lodash';
import { LocalStorageService } from './local-storage.service';
/**
* Cache storage record interface
*/
interface CacheStorageRecord {
expires: Date;
value: any;
}
@Injectable({
providedIn: 'root',
})
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).pipe(
//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
switchMap((val: CacheStorageRecord | null) => {
console.warn('flatMap', val);
if (val) {
return of(val.value);
} else {
return observable.pipe(
switchMap((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);
console.warn('value', value);
return this.localstorage
.setItem<CacheStorageRecord>(key, {
expires: _expires,
value: value,
})
.pipe(
map((item) => {
return item.value;
})
);
}
/**
*
* @param key
* @returns {Observable<null>}
*/
expire(key: string): Observable<void> {
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 as number) * 1000);
}
if (isString(expires)) {
return new Date(expires);
}
if (isDate(expires)) {
return expires as Date;
}
return new Date();
}
}
Thank you!!
Thank you!
Great!
@atdetquizan Thank you
Hi, Great stuff! you should package it.