Skip to content

Instantly share code, notes, and snippets.

@ckimrie
Last active December 12, 2023 20:53
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 19 You must be signed in to fork a gist
  • Save ckimrie/63334b6ad2873bd9db7ccbbf8ccdfd53 to your computer and use it in GitHub Desktop.
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.
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))
}
}
@Kevinlearynet
Copy link

Kevinlearynet commented Jan 7, 2019

@wengng

It's the localforage package, which this relies on as a dependency.

@zarpilla
Copy link

zarpilla commented Feb 1, 2019

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

@atdetquizan
Copy link

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

@yudgine2
Copy link

Thank you!!

@ranibb
Copy link

ranibb commented Jun 25, 2021

Thank you!

@alexandis
Copy link

Great!

@TaHaElyasi
Copy link

@atdetquizan Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment