5

I have a strange use case where I need to keep track of all previous emitted events.

Thanks to the ReplaySubject, it works perfectly so far. On every new subscriber, this Subject re-emits every previous events.

Now, for a specific scenario, I need to be able to give only the latest published events (a bit like a BehaviorSubject), but keeping the source the same events.

Here is a snippet of what I'm trying to achieve: stackblitz

import { ReplaySubject, BehaviorSubject, from } from "rxjs";

class EventManager {
  constructor() {
    this.mySubject = new ReplaySubject();
  }

  publish(value) {
    this.mySubject.next(value);
  }

  fullSubscribe(next, error, complete) {
    return this.mySubject.subscribe(next, error, complete);
  }

  subscribe(next, error, complete) {
    return this.mySubject.pipe(/* an operator to get the last one on new subscribe */).subscribe(next, error, complete);
  }
}

const myEventManager = new EventManager();

myEventManager.publish("Data 1");
myEventManager.publish("Data 2");
myEventManager.publish("Data 3");

myEventManager.fullSubscribe(v => {
  console.log("SUB 1", v);
});

myEventManager.subscribe(v => {
  console.log("SUB 2", v);
});

Thank you

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
Anas Bud
  • 347
  • 4
  • 15
  • The RxJS#`last()` operator does this – Mrk Sef Apr 30 '21 at 15:40
  • @MrkSef not exactly. `last()` won't work the way they're requesting, it just outputs the last published event before the observable completes. OP is requesting that when the `ReplaySubject` is subscribed to, the "hot" subject immediately emits the last event that was published (like `BehaviorSubject`) rather than all the events that have ever been published to it like default. – Patrick Roberts Apr 30 '21 at 15:45
  • 1
    @PatrickRoberts Good point! Okay, use the RxJS#`debounceTime(0)`. I think `0` should work here as the replay happens synchronously. If not, he can up the debounce time a little bit. – Mrk Sef Apr 30 '21 at 17:27
  • I thought about debounceTime(0), the problem with that is that if subsequent emissions happen on the same tick, only the last one will be received. – BizzyBob Apr 30 '21 at 17:28

3 Answers3

3

If you keep track of the number of events you've published, you could use skip:

  subscribe(next, error?, complete?) {
    return this.mySubject.pipe(
      skip(this.publishCount - 1)
    ).subscribe(next, error, complete);
  }

Here's a StackBlitz demo.

BizzyBob
  • 12,309
  • 4
  • 27
  • 51
1

Instead of forcing a ReplaySubject to behave like a BehaviorSubject, you can arrive at ReplaySubject-like behavior by manipulating a BehaviorSubject.

import { BehaviorSubject, from, concat } from 'rxjs';
import { scan, shareReplay } from 'rxjs/operators';

class EventManager {
  constructor() {
    this.mySubject = new BehaviorSubject();
    this.allEmittedValues = this.mySubject.pipe(
      scan((xs, x) => [...xs, x], []),
      shareReplay(1)
    );

    // Necessary since we need to start accumulating allEmittedValues
    // immediately.
    this.allEmittedValues.subscribe();
  }

  dispose() {
    // ends all subscriptions
    this.mySubject.complete();
  }

  publish(value) {
    this.mySubject.next(value);
  }

  fullSubscribe(next, error, complete) {
    // First, take the latest value of the accumulated array of emits and
    // unroll it into an observable
    const existingEmits$ = this.allEmittedValues.pipe(
      take(1),
      concatMap((emits) => from(emits))
    );
    // Then, subscribe to the main subject, skipping the replayed value since
    // we just got it at the tail end of existingEmits$
    const futureEmits$ = this.mySubject.pipe(skip(1));

    return concat(existingEmits$, futureEmits$).subscribe(
      next,
      error,
      complete
    );
  }

  subscribe(next, error, complete) {
    return this.mySubject.subscribe(next, error, complete);
  }
}
backtick
  • 2,685
  • 10
  • 18
0

Why not just have an instance of ReplaySubject and BehaviorSubject on EventManager?

import { ReplaySubject, BehaviorSubject, from } from "rxjs";

class EventManager {
  constructor() {
    this.replaySubject = new ReplaySubject();
    this.behaviorSubject = new BehaviorSubject();
  }

  publish(value) {
    this.replaySubject.next(value);
    this.behaviorSubject.next(value);
  }

  fullSubscribe(next, error, complete) {
    return this.replaySubject.subscribe(next, error, complete);
  }

  subscribe(next, error, complete) {
    return this.behaviorSubject.subscribe(next, error, complete);
  }
}
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Because in reality, it's more complicated. I have to deal with an array of Subjects. By introducing another BehaviorSubject, it will increase the memory load too much. I was hoping for a "simple" way to handle that... – Anas Bud Apr 30 '21 at 15:01
  • @fred.kassi the `ReplaySubject` is using a lot of memory sure, but by comparison the `BehaviorSubject` is only holding an extra reference to a value already held by `ReplaySubject`, so the additional memory is essentially constant per `BehaviorSubject`. Unless you happen to be publishing really large strings for some reason, I suspect you won't notice a significant difference in memory usage. – Patrick Roberts Apr 30 '21 at 15:22