4

I hold state in one ReplaySubject that replays the last copy of the state. From that state, other ReplaySubjects are derived to hold...well, derived state. Each replay subject need only hold it's last calculated state/derived state. (We don't use BehaviorSubjects because they always give a value, but we only want a value derived from our parent observables.) It is always necessary to replay the value to new subscribers if we have already generated derived state.

I have a custom observable operator that accomplishes this in just the way I want it to, but it doesn't feel that clean. I feel like there should be an efficient way to accomplish this with RxJ's operators themselves.

I have tried the two most obvious approaches, but there are slight problems with each. The problem involves unsubscribing and re-subscribing. Open the fiddle below, open your console, and click run. I will describe the problem with each output.

https://jsfiddle.net/gfe1nryp/1/

The problem with a refCounted ReplaySubject

=== RefCounted Observable ===


Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Resubscribe
Subscription 2: 3
Work
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8

This works well, the intermediate functions don't do any work when there is nothing subscribed. However, once we resubscribe. We can see that Subscription 2 replays the last state before unsubscribe, and then plays the derived state based on the current value in the base$ state. This is not ideal.

The problem with connected ReplaySubject

=== Hot Observable ===
Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Work
Work
Work
Resubscribe
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8

This one does not have the same problem as the refCounted observable, there is no unnecessary replay of the last state before the unsubscription. However, since the observable is now hot, the tradeoff is that we always do work whenever a new value comes in the base$ state, even though the value is not used by any subscriptions.

Finally, we have the custom operator:

=== Custom Observable ===
Work
Subscription 1: 1
Work
Subscription 1: 2
Work
Subscription 1: 3
Unsubscribe
Resubscribe
Work
Subscription 2: 6
Work
Subscription 2: 7
Work
Subscription 2: 8

Ahh, the best of both worlds. Not only does it not unnecessarily replay the last value before unsubscription, but it also does not unnecessarily do any work when there is no subscription. This is accomplished by manually creating a combination of RefCount and ReplaySubject. We keep track of each subscriber, and when it hits 0, we flush the replay value. The code for it is here (and in the fiddle, of course):

Rx.Observable.prototype.selectiveReplay = function() {
  let subscribers = [];
  let innerSubscription;

  let storage = null;

  return Rx.Observable.create(observer => {
    if (subscribers.length > 0) {
      observer.next(storage);
    }

    subscribers.push(observer);

    if (!innerSubscription) {
      innerSubscription = this.subscribe(val => {
        storage = val;
        subscribers.forEach(subscriber => subscriber.next(val))
      });
    }

    return () => {
      subscribers = subscribers.filter(subscriber => subscriber !== observer);

      if (subscribers.length === 0) {
        storage = null;
        innerSubscription.unsubscribe();
        innerSubscription = null;
      }
    };
  });
};

So, this custom observable already works. But, can this be done with only RxJS operators? Keep in mind, potentially there could be more than a couple of these subjects linked together like this. In the example, I'm only using one linking to the base$ to illustrate the issue with both vanilla approaches I've tried at the most basic level. Basically, if you can use only RxJS operators, and get the output to match the output for === Custom Observable === above. That's what I'm looking for. Thanks!

user3743222
  • 18,345
  • 5
  • 69
  • 75
aaronofleonard
  • 2,546
  • 17
  • 23

1 Answers1

4

You should be able to use multicast with a subject factory instead of a subject. Cf. https://jsfiddle.net/pto7ngov/1/

(function(){
  console.log('=== RefCounted Observable ===');
  var base$ = new Rx.ReplaySubject(1);

  var listen$ = base$.map(work).multicast(()=> new Rx.ReplaySubject(1)).refCount();

  var subscription1 = listen$.subscribe(x => console.log('Subscription 1: ' + x));

  base$.next(1);
  base$.next(2);
  base$.next(3);

    console.log('Unsubscribe');
  subscription1.unsubscribe();

  base$.next(4);
  base$.next(5);
  base$.next(6);

    console.log('Resubscribe');
  var subscription2 = listen$.subscribe(x => console.log('Subscription 2: ' + x));

  base$.next(7);
  base$.next(8);
})();

This overload of the multicast operator serves exactly your use case. Every time the observable returned by the multicast operator completes and is reconnected to, it creates a new subject using the provided factory. It is not very well documented though, but it basically replicates an existing API from Rxjs v4.

In case I misunderstood or that does not work let me know,

user3743222
  • 18,345
  • 5
  • 69
  • 75
  • No misunderstanding here, it works exactly as intended! Thank you very much. I tried messing around with `multicast` but I didn't really know how it works; I didn't know what made it especially different from `publish`. I didn't even realize you could pass a factory for Subjects! That helps a lot. So rather than trying to get the `ReplaySubject` to remove it's state upon unsubscribe, we just throw that sucker away on subscribe, and get a new one if we resubscribe. Beautiful! Thanks :) – aaronofleonard Mar 20 '17 at 15:17
  • `sourceObservable.publish().refCount()` is a shorthand way of writing `sourceObservable.multicast(new Rx.Subject()).refCount()`. And `sourceObservable.share()` is a shorthand way of writing `sourceObservable.publish().refCount()` – Robert Jul 02 '17 at 19:58