1

Scenario:

  1. User uses filters which are combined into single stream
  2. When filters change, event to the backend is fired to get "cheap" data
  3. When "cheap" data arrives, another request, with same parameters is fired to different endpoint, that returns "expensive" data which will be used to enrich cheap data. Request should be delayed by 1 second, and only fired if user does not change any of the filters (else it should wait for 1 second)

And i'm struggling with 3) option without intermediate variables.

let filterStream = Rx.Observable
.combineLatest(
  filterX,
  filterY,
  (filterX, filterY) => {
    x: filterX,
    y: filterY
  }
 )
 .map((filters) => {
  limit: 100,
  s: filters.x.a,
  f: filters.x.b + filters.y.c,
})
.distinctUntilChanged()


let cheapDataStream = filterStream
.flatMapLatest((filterQuery) =>
Rx.Observable.fromPromise(cheapBackendApiCall(filterQuery)))

// render cheap results
cheapDataStream
.map(result => transformForDisplay(result))
.subscribe(result => { 
  //render
  // how do i invoke expensiveApiCall() with `filterQuery` data here?
  // with a delay, and only if filterQuery has not changed?

});
gerasalus
  • 7,538
  • 6
  • 44
  • 66
  • So does expensive data = (cheap data + extra) or is expensive data = extra and you need to combine them yourself? – paulpdaniels Mar 22 '16 at 18:31
  • expensive data is extra, and i need to combine it. Let's say that cheap data is product list, and expensive data is availability of those products - initially you get all products, and then you get availability which is only returned if product has one. – gerasalus Mar 22 '16 at 18:58

3 Answers3

2

You can take advantage of implicit conversion to avoid explicitly using fromPromise everywhere. Then you could use concat to return first the cheap data immediately, followed by the expensive + cheap data with a delay. By nesting this in a flatMapLatest the stream will also cancel any pending expensiveCalls if a new query arrives.

var filters = Rx.Observable
.combineLatest(
  filterX,
  filterY,
  (filterX, filterY) => {
    x: filterX,
    y: filterY
  }
 )
 .map((filters) => {
  limit: 100,
  s: filters.x.a,
  f: filters.x.b + filters.y.c,
})
.distinctUntilChanged()
.flatMapLatest(filters => {
  //This kicks off immediately
  var cheapPromise = cheapBackendApiCall(filters);

  //This was added in the latest version 4.1, the function is only called once it is subscribed to, 
  //if you are using earlier you will need to wrap it in a defer instead.
  var expensivePromiseFn = () => expensiveBackendApiCall(filters);

  //For join implicitly calls `fromPromise` so you can pass the same 
  // sort of arguments.
  var cheapAndExpensive = Rx.Observable.forkJoin(
                            cheapPromise, 
                            expensivePromiseFn, 
                            (cheap, expensive) => ({cheap, expensive}));

  //First return the cheap, then wait 1500 millis before subscribing 
  //which will trigger the expensive operation and join it with the result of the cheap one
  //The parent `flatMapLatest` guarantees that this cancels if a new event comes in
  return Rx.Observable.concat(cheap, cheapAndExpensive.delaySubscription(1500));
})
.subscribe(x => /*Render results*/);
paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • Nice. Only downside is that forkJoin is only included in rx.all.js which adds +10kb for the library compared to rx.lite.js – gerasalus Mar 23 '16 at 06:03
  • I haven't been keeping tabs on the modular work but I believe there was recently an addition to be able to do custom builds with only the operators you need. – paulpdaniels Mar 23 '16 at 07:19
0

Do you looking for debounce?
This operator seems to do exactly what you described.

Curious Sam
  • 884
  • 10
  • 12
  • If i add debounce() to cheapDataStream it will throttle cheapDataStream also, which i do not want. If i create new Rx.Observable in subscribe method, debounce does help. Maybe i could use takeUntil in subscribe with new observable, that does not see to work if while we are delaying, user changes some filters – gerasalus Mar 22 '16 at 11:38
  • @gerasalus you can solve it in various way. For instance you create 2 subscription cheap and full. You set just the basic info in cheap subscription without debouncing and another subscription for full info after debounce. – Curious Sam Mar 22 '16 at 14:14
0

Solution that i've come up with, not sure though if do() is appropriate, and cheap + expensive data merging does not look very reactish.

Rx.Observable
.combineLatest(
  filterX,
  filterY,
  (filterX, filterY) => {
    x: filterX,
    y: filterY
  }
 )
 .map((filters) => {
  limit: 100,
  s: filters.x.a,
  f: filters.x.b + filters.y.c,
})
.distinctUntilChanged()
.flatMapLatest((filterQuery) =>
  Rx
  .Observable
  .fromPromise(cheapBackendApiCall(filterQuery))
  .map((results) => {
    filterQuery: filterQuery,
    results: results
  })
)
.do((filtersAndResults) => {
  // render filtersAndResults.results
})
.debounce(1500)
.flatMapLatest((filtersAndResults) => {
  return Rx
  .Observable
  .fromPromise(expensiveBackendApiCall(filtersAndResults.filterQuery))
  .map(results => {
    expensiveData: results,
    cheapData: filtersAndResults.results
  })
})
.subscribe((result)=> {
  // combine results.cheapData + results.expensiveData with simple .map and .find
  //  and render
})
gerasalus
  • 7,538
  • 6
  • 44
  • 66