1

I’m looking for an RxJS example how to cache a series of XHR calls (or other async operations), so the same call does not have to be repeated, while respecting immutability and with no side effects.

Here's a bare-bones, mutable example:

var dictionary = {}; // mutable

var click$ = Rx.Observable.fromEvent(document.querySelector('button'), 'click', function (evt) {
  return Math.floor(Math.random() * 6) + 1; // click -> random number 1-6 (key)
})
  .flatMap(getDefinition);

var clicksub = click$.subscribe(function (key) {
  console.log(key);
});

function getDefinition (key) {
  if ( dictionary[key] ) { // check dict. for key
    console.log('from dictionary');
    return Rx.Observable.return(dictionary[key]);
  }
  // key not found, mock up async operation, to be replaced with XHR
  return Rx.Observable.fromCallback(function (key, cb) {
    dictionary[key] = key; // side effect
    cb(dictionary[key); // return definition
  })(key);
}

JSBin Demo

Question: Is there a way to accomplish caching several similar async operations without resorting to using the dictionary variable, due to mutability and side effect?


I’ve looked at scan as a means to “collect” the XHR call results, but I don’t see how to handle an async operation within scan.

I think I’m dealing with two issues here: one is state management maintained by the event stream rather than kept in a variable, and the second is incorporating a conditional operation that may depend on an async operation, in the event stream flow.

bloodyKnuckles
  • 11,551
  • 3
  • 29
  • 37

2 Answers2

2

Using the same technique than in a previous question (RxJS wait until promise resolved), you could use scan and add the http call as a part of the state you keep track of. This is untested but you should be able to easily adapt it for tests :

restCalls$ = click$
  .scan(function (state, request){
    var cache = state.cache;
    if (cache.has(request)) {
      return {cache : cache, restCallOrCachedValue$ : Rx.Observable.return(cache.get(request))}
    }
    else {
      return {
        cache : cache,
        restCallOrCachedValue$ : Rx.Observable
          .defer(function(){
            return Rx.Observable
              .fromPromise(executeRestCall(request))
              .do(function(value){cache.add(request,value)})
          })
      }
    }
  }, {cache : new Cache(), restCallOrCachedValue$ : undefined})
  .pluck('restCallOrCachedValue$')
  .concatAll()

So basically you pass an observable which does the call down the stream or you directly return the value in the cache wrapped in an observable. In both cases, the relevant observables will be unwrapped in order by concatAll. Note how a cold observable is used to start the http call only at the time of the subscription (supposing that executeRestCall executes the call and returns immediately a promise).

Community
  • 1
  • 1
user3743222
  • 18,345
  • 5
  • 69
  • 75
  • I'm getting an error using `defer`, but `fromPromise` (in conjunction with an XHR call that returns a promise) does pass the retrieved value on down the stream. However, there still remains the problem of how to get the retrieved value from the async operation (promise, XHR...) into the cache. Just adding the key to the cache, as in your answer, does not save the retrieved value for future use. – bloodyKnuckles May 12 '16 at 23:32
  • updated the code. What is it that is not working with defer? My guess is that the function passed as a parameter must return an observable and it was returning a promise. It should work now. The main insights in any case are three : 1. wrap the execution in a function (i.e. defer the execution of the call) 2. pass the stream as part of the state 3. use `concatAll` to have results in order. About Insight 1, you could execute the call directly but then you are executing a side-effect within the `scan`. Nothing wrong with that, but I prefer to use a pure function in the `scan`. – user3743222 May 13 '16 at 08:08
  • That helps a lot. Question, what is the purpose of `defer` ? I've pulled it out (and brought `fromPromise` forward) and the caching works as desired (see my updated answer). – bloodyKnuckles May 13 '16 at 13:51
  • For me this is an exercise of learning about immutability and no side effects. I'm satisfied there are no side effects but is this still mutating the cache object? – bloodyKnuckles May 13 '16 at 13:55
  • Disregard my question about `defer`, I just need to keep reading your comment. :) – bloodyKnuckles May 13 '16 at 14:01
  • 1
    `defer` returns an observable which when subscribed to executes a function returning an observable and subscribes to that observable. That is useful in a case like this, to defer the execution of `executeRestCall`. Otherwise without defer, the http call would be executed straight away hence producing a side-effect within the `scan`. With `defer` it is executed when `concatAll` subscribes to it. For the answer to the other, see the provided link in your provided answer. Also don't forget to upvote if the answer was useful, and accept it (or your answer) if it is doing what you specified. – user3743222 May 13 '16 at 14:01
  • The short answer is that yes, your cache is modified, but it is fine because it can never be modified out of where it was defined. The problem with mutability is not mutability, it is unexpected or invisible mutability. The problem with state is not state, it is hidden state, etc. – user3743222 May 13 '16 at 14:07
0

Working from @user3743222's response, this code does cache the XHR calls:

// set up Dictionary object
function Dictionary () { this.dictionary = {} }
Dictionary.prototype.has = function (key) { return this.dictionary[key] }
Dictionary.prototype.add = function (key, value) { this.dictionary[key] = value }
Dictionary.prototype.get = function (key) { return this.dictionary[key] }

var definitions$ = click$

  .scan(function (state, key) {
    var dictionary = state.dictionary

    // check for key in dict.
    if ( dictionary.has(key) ) {
      console.log('from dictionary')
      return {
        dictionary: dictionary, 
        def$: Rx.Observable.return(dictionary.get(key))
      }
    }

    // key not found
    else {
      return {
        dictionary: dictionary, 
        def$: Rx.Observable.fromPromise(XHRPromise(key))
          .do(function (definition) { dictionary.add(key, definition) })
      }
    }

  }, {dictionary : new Dictionary(), def$ : undefined})

  .pluck('def$') // pull out definition stream
  .concatAll()   // flatten

Updated: In the // key not found block, executing the function XHRPromise (that returns a promise) passing in the key and the result passed to the RxJS method fromPromise. Next, chain a do method in which the definition is grabbed and added to the dictionary cache.

Follow up question: It appears we've removed the side effect issue, but is this still considered mutating when the definition and key are added to the dictionary?


Putting this here for archival purposes, the first iteration had this code, accomplishing the same caching procedure:

    // ALTERNATIVE key not found
    else {
      var retdef = Rx.Observable.fromPromise(function () {
        var prom = XHRPromise(key).then(function (dd) {
          dictionary.add(key, dd) // add key/def to dict.
          return dd
        })
        return prom
      }()) // immediately invoked anon. func.
      return { dictionary: dictionary, def$: retdef }
    }

Here, using the function XHRPromise that returns a promise. Before returning that promise to the event stream, though, pull out a then method in which the promised definition is grabbed and added to the dictionary cache. After that the promise object is returned to the event stream through the RxJS method fromPromise.

To get access to the definition returned from the XHR call, so it can be added to the dictionary cache, an anonymous, immediately invoked function is used.

bloodyKnuckles
  • 11,551
  • 3
  • 29
  • 37
  • cf. http://stackoverflow.com/questions/36141280/how-to-manage-state-without-using-subject-or-imperative-manipulation-in-a-simple/36142372#36142372 – user3743222 May 13 '16 at 07:54