EHorodyski
EHorodyski

Reputation: 794

Cache Fallback HTTP Requests

I'm having a lot of trouble figuring out the sequence to a Service I started building. What I would like to do is load cache from a Promise-based storage provider, then start an observable with that cache. Once the HTTP request resolves, I then want to replace the old cache with the new response.

I'm having a really hard time figuring out this process and would appreciate any help.

PSEUDOCODE

import {Injectable} from "@angular/core";
import {Http} from "@angular/http";
import rxjs/add/operators/map;
import rxjs/add/operators/startWith;
import {Storage} from "@ionic/storage";

@Injectable()
export class DataService {
    private _cache = {};

    constructor(public http: Http; public storage: Storage){}

    public getCache(): Promise<any> {
        return this.storage.get('storageKey');
    }

    public setCache(val: any): Promise<any> {
        return this.storage.set('storageKey', val);
    }

    public getData(): Observable<any> {
        // Step 1: Get cache.
        this.getCache().then(cache => this._cache = cache);

        // Step 2: Make HTTP request.
        return this.http.get('endpoint')
        // Step 3: Set data mapping
           .map(res => this.normalize(res))
        // Step 4: Use resolved cache as startWith value
           .startWith(this._cache)
        // Step 5: Set cache to response body.
           .do(result => this.setCache(result).then(() => console.log('I resolved'));    
    }

    private normalize(data: Response): any {
       // Fun data-mapping stuff here
    }
}

This code is meant to run on a mobile device, where the illusion of speed, plus offline capabilities are #1 priorities. By initially setting a view with old cache, the user can view what has been stored, and if the HTTP request fails (slow network, or no network), no big deal, we fallback to the cache. However, if the user is online, we should use this opportunity to replace the old cache with the new response from the HTTP request.

Thanks for any and all help, and please let me know if I need to elaborate further.

Upvotes: 1

Views: 1015

Answers (3)

EHorodyski
EHorodyski

Reputation: 794

Taken from Richard Matsen, this is the final result I came to. I added a catch clause, which might be a bit superfluous, but using Ionic on the web, you will get their debug screen, so that's why it's in there. Can the catch be improved so that the do doesn't run or something?

public getFeed(source: SocialSource): Observable<any> {
    const cached$ = Observable.fromPromise(this._storage.get(`${StorageKeys.Cache.SocialFeeds}.${source}`));
    const fetch$ = this._http.get(`${Configuration.ENDPOINT_SOCIAL}${source}`)
        .map(res => this.normalizeFacebook(res))
        .catch(e => cached$)
        .do(result => this._storage.set(`${StorageKeys.Cache.SocialFeeds}.${source}`, result));
    return cached$.concat(fetch$);
}

Upvotes: 0

Richard Matsen
Richard Matsen

Reputation: 23473

If I've understood the requirement correctly,

Edit - adjusted following discussion

getData() {

  // storage is the cache
  const cached$ = Observable.fromPromise(this.storage.get('data'))  

  // return an observable with first emit of cached value
  // and second emit of fetched value.
  // Use concat operator instead of merge operator to ensure cached value is emitted first,
  // although expect storage to be faster than http 
  return cached$.concat(
    this.http.get('endpoint')
      .map(res => res.json())
      .do(result => this.storage.set('data', result))
  );
}

Edit #2 - Handling initially empty cache

After testing in a CodePen here, I realized there's an edge-case that means we should use merge() instead of concat(). If storage is initially empty, concat will block because cached$ never emits.

Additionally, need to add distinctUntilChanged() because if storage is initially empty, merge will return the first fetch twice.

You can check out the edge-case by commenting out the line that sets 'prev value' in the CodePen.

/* Solution */
const getData = () => {
  const cached$ = Observable.fromPromise(storage.get('data'))  

  return cached$ 
    .merge(
      http.get('endpoint')
        .map(res => res.json())
        .do(result => storage.set('data', result))
    )
    .distinctUntilChanged()
}
/* Test solution */
storage.set('data', 'prev value')  // initial cache value
setTimeout( () => { getData().subscribe(value => console.log('call1', value)) }, 200)
setTimeout( () => { getData().subscribe(value => console.log('call2', value)) }, 300)
setTimeout( () => { getData().subscribe(value => console.log('call3', value)) }, 400)

Upvotes: 2

Fan Cheung
Fan Cheung

Reputation: 11345

Use concat to ensure the order ?

const cache = Rx.Observable.of('cache value');
const http = function(){

  return Rx.Observable.of('http value');
                        };
var storage={}
storage.set=function(value){
 return Rx.Observable.of('set value: '+value);
}
const example = cache.concat(http().flatMap(storage.set));
const subscribe = example.subscribe(val => console.log('sub:',val));`

jsbin: http://jsbin.com/hajobezuna/edit?js,console

Upvotes: 0

Related Questions