Peter Albert Peter Albert - 27 days ago 12
Javascript Question

Other operator in calculation chain than combineLatest to avoid redundant calculations

I've successfully migrated a larger Excel calculation to JS using RxJS. Any input data is represented as an Observable and subsequent calculations are implemented with

.map
and
.combineLatest
when any Excel formula uses more than one input.

This works great except for one problem. Here is a simplified example:
Excel screenshot

Three inputs (
a$=1
,
b$=2
,
c$=3
) are used in two different calculations (
$ab = $a+$b = 3
in the first,
$bc = $b+$c = 5
in the second) as intermediate steps to calculate the final result
$abbc = $ab + $bc = 8
.

When $b is now updated/emitting a new input value (e.g.
4
), $abbc is calculated twice - first when $ab is updated (resulting in the wrong result
$abbc=10
) and again when $bc is updated, resulting in the correct result
12
.

While the final result is correct, the intermediate calculation is both wrong and redundant. Is there any way to only execute the last calculation in case
$b
gets updated - while still also updating the calculation when
a$
or
c$
is updated (which would rule out the zip operator). I understand that this example can obviously be simplified to leave out the intermediate steps and calculated
$abbc
directly from
$a
,
$b
and
$c
- but in the real live example this is not possible/feasible.

Here's the running example in JSbin: https://jsbin.com/pipiyodixa/edit?js,console

Answer

The problem here is that RxJS's behavior is correct by its design.

It really should first update b => ab => abbc and then bc => abbc. It processed values in streams.

What you want is to process values in "layers".a, b, c, then ac , bc and after that calculate final value abbc.

The only way I can think of is to make use of JavaScript execution context and a trick with setTimeout(() => {}, 0). This way you don't schedule any timeout (in fact the real timeout might be > 0) and just run the closure in another execution context after JavaScript finishes executing the current one.

The bad thing is that to avoid re-emitting values multiple times you need to cache even more (because of merge()):

var b2$ = b$.cache(1);

var ab$ = a$
  .combineLatest(b2$, (a, b) => a + b)
  .do(x => console.log('$a + $b = ' + x))
  .cache(1);

var bc$ = b2$
  .combineLatest(c$, (b, c) => b + c)
  .do(x => console.log('$b + $c = ' + x))
  .cache(1);

var abbc$ = new Rx.Observable.merge(ab$, bc$)
  .auditTime(0)
  .withLatestFrom(ab$, bc$, (_, ab, bc) => ab + bc)
  .do(x => console.log('$ab + $bc = ' + x));

console.log("Initial subscription:")
abbc$.subscribe();

b$.next(4);

The auditTime() operator is the most important thing here. It triggers withLatestFrom() to update it's value while when it's triggered for first time by ab$ or bc$ it ignores all consecutive emits until the end of this closure execution (that's the setTimeout() trick).

See live demo: https://jsbin.com/zoferid/edit?js,console

Also, if you add a$.next(5); the final calculation is executed just once (which might be both good or bad :)).

I don't know if this solves your problem because as I said RxJS works this way. I haven't tested it on any more complicated example so maybe this is not the way you can use at the end.

Note that cache() operator was removed in RC.1 and there's no replacement for now: https://github.com/ReactiveX/rxjs/blob/master/CHANGELOG.md

Comments