Unexpected withLatestFrom operator behavior

Bug Report

Current Behavior

const bh1 = new BehaviorSubject<string>(null);
const bh2 = new BehaviorSubject<string>(null);

bh1.pipe(filter(Boolean), tap(console.log)).subscribe(bh2);
bh2.pipe(withLatestFrom(bh1)).subscribe(console.warn);

bh1.next('BH');

You’ll see:
image

bh1 value was updated but withLatestFrom operator took previous (null). The problem can be solved using debounceTime:

bh2.pipe(debounceTime(0), withLatestFrom(bh1)).subscribe(console.warn);

But I don’t want to use such “workarounds” for many reasons.

Expected behavior
withLatestFrom should pull the latest state even if it was updated synchronously.

Environment

  • Runtime: Chrome v83.0.4103.116
  • RxJS version: v6.5.5

1 possible answer(s) on “Unexpected withLatestFrom operator behavior

  1. Hi @stas-karmanov ,

    This is not a bug. Have you noticed that another way of getting the behavior that you are looking for is to change the order of the subscriptions? Like this:

    const bh1 = new BehaviorSubject<string>(null);
    const bh2 = new BehaviorSubject<string>(null);
    
    bh2.pipe(withLatestFrom(bh1)).subscribe(console.warn);
    bh1
      .pipe(
        filter(Boolean),
        tap(console.log)
      )
      .subscribe(bh2);
    
    bh1.next("BH");

    If changing the order of the subscriptions is not an option, then you could also observe bh1 through the asapScheduler, like this:

    const bh1 = new BehaviorSubject<string>(null);
    const bh2 = new BehaviorSubject<string>(null);
    
    bh1
      .pipe(
        observeOn(asapScheduler),
        filter(Boolean),
        tap(console.log)
      )
      .subscribe(bh2);
    bh2.pipe(withLatestFrom(bh1)).subscribe(console.warn);
    
    bh1.next("BH");

    If you think about it carefully, it does make sense that your code is behaving in the way that’s behaving. Notice that this line:

    bh2.pipe(withLatestFrom(bh1)).subscribe(console.warn);

    is roughly the equivalent of:

    new Observable(observer => {
      let latestVal: string | null;
      const bh1Subscription = bh1.subscribe(x => {
        latestVal = x;
      });
    
      const bh2Subscription = bh2.subscribe({
        next(val) {
          observer.next([val, latestVal] as const);
        },
        complete() {
          observer.complete();
        },
        error(e: any) {
          observer.error(e);
        }
      });
      return () => {
        bh2Subscription.unsubscribe();
        bh1Subscription.unsubscribe();
      };
    }).subscribe(console.warn);

    Notice that there are 2 subscribers listening to bh1? One that you have created yourself and the one that withLatestFrom has created, which in the “raw” version above I have named bh1Subscription. Since “yours” got subscribed first, it will also get notified first, right?

    That means that when it gets notified, it will synchronously perform a next to bh2, whicn in its turn will immediately notify the bh2Subscription. Therefore it’s only after this subscription chain finishes that the bh1Subscription will be notified. That’s why switching the order of the subscriptions produces the behavior that you are after. Because that makes the subscription created by the withLatestFrom operator to get subscribed (and notified) first.