2

I need to cache the result of a request on the first call then read the cached value for subsequent calls.

To achieve that goal, I am using promises and I am chaining them. I have a working solution but I would like to convert it to RxJS's observables instead of Promises.

Here is my working solution:

private currentPromise: Promise<{ [key: string]: any }>;
private cache: any;
public getSomething(name: string): Promise<number>{
  return this.currentPromise = !this.currentPromise ? 
    this._getSomething(name) : 
    new Promise((r) => this.currentPromise.then(() => this._getSomething(name).then((res) => r(res))));
}

private _getSomething(name: string): Promise<any> {
  return new Promise((resolve) => {
    if (this.cache[name]) {
      this.messages.push("Resolved from cache");
        resolve(this.cache[name]);
      } else {
        // Fake http call. I would use Angular's Http class.
        setTimeout(()=> {this.messages.push("Resolved from server"); this.cache[name] = name; resolve(this.cache[name]); }, 2000 );
      }
  });
}

this.getSomething("thing1").then((res)=>this.messages.push(res));
this.getSomething("thing1").then((res)=>this.messages.push(res));
this.getSomething("thing2").then((res)=>this.messages.push(res));
this.getSomething("thing2").then((res)=>this.messages.push(res));
this.getSomething("thing1").then((res)=>this.messages.push(res));
this.getSomething("thing2").then((res)=>this.messages.push(res));
this.getSomething("thing1").then((res)=>this.messages.push(res));
this.getSomething("thing2").then((res)=>this.messages.push(res));

You can test it on this plunkr: https://plnkr.co/edit/j1pm2GeQf6oZwRvbUsXJ?p=preview

How do I achieve the same thing with RxJS 5 beta?

Update

Following Bergi's comments I updated my plunkr and my code to bring it closer to my real case

Jean-Philippe Leclerc
  • 6,713
  • 5
  • 43
  • 66
  • "*`new Promise((r) => this.currentPromise.then(() => this._getSomething(params).then((res) => r(res))));`*" - what? What is this supposed to do (and: [don't do it](http://stackoverflow.com/q/23803743/1048572))? Why not just return the `currentPromise`? – Bergi Apr 19 '16 at 20:47
  • If you are already caching `currentPromise`, there's no need to additionally cache the value itself in `.cache`. Don't overcomplicate it. – Bergi Apr 19 '16 at 20:48
  • @Bergi Everytime "getSomething" is called, I append a new ".then()" to the currentPromise so all "getSomething" calls are executed in the same order they are called. The first call fetches the data from the server and caches it. The subsequent calls returns the data from the cache. – Jean-Philippe Leclerc Apr 19 '16 at 20:52
  • You don't need to execute them multiple times - you've already got the (promise for the) result of the first call in `currentPromise`. You can simply return it multiple times. It will invoke all callbacks in the same order they were attached implicitly. – Bergi Apr 19 '16 at 20:55
  • I my real world case, _getSomething has to be called everytime because "params" can change the result. My cache is actually a dictionnary. So if I call getSomething("thing1") it will check if "thing1" is already in the cache, if not it will make a call to the server. – Jean-Philippe Leclerc Apr 19 '16 at 21:01
  • @Bergi I just updated my code, hopefully it makes more sense. – Jean-Philippe Leclerc Apr 19 '16 at 21:10
  • Still not really. Just `return this.cache[name] || (this.cache[name] = this._getSomething(name);` and `return new Promise(r => setTimeout(r, 2000, name));` – Bergi Apr 19 '16 at 21:50
  • @Bergi This is very clever I didn't think about caching the whole promise. I still don't know if it will work for me since params can be an array of things to fetch so multiple keys could be retreived by the same request. I will see what I can do with your solution. – Jean-Philippe Leclerc Apr 19 '16 at 22:07

1 Answers1

1

AsyncSubjects are the Rx analog of Promises. publishLast is the best way to turn an observable into one. Something like this should work:

private cache: {string: Rx.Observable<any>};

public getSomethings(names: string[]) : Rx.Observable<any> {
    // call getSomething for each entry in names
    // streams is an array of observables
    const streams = names.map(name => this.getSomething(name));

    // transform streams into an observable with an array of results
    return Observable.zip(streams);
}

public getSomething(name: string) : Rx.Observable<any> {
    if (!this.cache[name]) {
        // create the request observable
        // const request = Rx.Observable.ajax(...); // http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-ajax
        // for now, just wait 2 seconds and return name
        const request = Rx.Obsevable.of(name).delay(2000);

        // use "do" to log whenever this raw request produces data
        const loggedRequest = request.do(v => this.messages.push("retrieved from server " + v));

        // create an observable that caches the result
        // in an asyncSubject
        const cachedRequest = loggedRequest.publishLast();

        // store this in our cache object
        this.cache[name] = cachedRequest;
    }

    // return the cached async subject
    return this.cache[name];
}


// usage
this.getSomething("thing1").subscribe(v => this.messages.push("received " + v));
this.getSomething("thing1").subscribe(v => this.messages.push("received " + v));
this.getSomething("thing1").subscribe(v => this.messages.push("received " + v));
this.getSomething("thing1").subscribe(v => this.messages.push("received " + v));
Brandon
  • 38,310
  • 8
  • 82
  • 87
  • Thank you this is very useful. I know I didn't include that in my question, but how would you handle the case where "getSomething" can receive an array of things to get in parameter. Example: getSomething(["thing1", "things2"]) would get both from server then getSomething(["thing1", "thing3"]) would return thing1 from cache and thing3 from server. – Jean-Philippe Leclerc Apr 20 '16 at 16:49
  • what would you expect the observable to produce? An array of results? – Brandon Apr 20 '16 at 16:50
  • Exactly. I'm not sure how it would work, but I guess it would use the next() to aggregate the "things" in an array then call the complete() function to trigger the subscriber when they are ready. – Jean-Philippe Leclerc Apr 20 '16 at 16:54
  • I added a `getSomethings` that shows how you might take an array of inputs and return an observable that produces a single array with the results. It uses `getSomething` for each item, so you still get the caching benefits. – Brandon Apr 20 '16 at 17:03
  • thank you for your update. Your solution works very well but it is still not perfect for my case. The server actually takes the array of string and returns an array of "things". Example: Lets says that "thing1" and "thing2" are cached. Then I call getSomethings(["thing1","thing2","thing3","thing4"]), it has to send ["thing3","thing4"] to the server, wait for the reponse then combine ["thing1", "thing2"] from the cache with ["thing3","thing4"] from the server. In the end I would like to be able to preload some of my "things" on application start. – Jean-Philippe Leclerc Apr 20 '16 at 17:38