Can this rxjs merge logic be simplified?

Viewed 159

I have an observable (onAuthStateChanged) from the Firebase client that:

  1. emits null immediately if the user is not signed in, and
  2. emits null and then a user object a few moments later if the user is signed in.
const observable = new Observable((obs) => {
    return app.auth().onAuthStateChanged(
        obs.next,
        obs.error,
        obs.complete
    )
})

What I want is to:

  1. ignore any emitted null values for the first 1000ms of the app lifecycle (null coming after 1000ms is accepted)
  2. always emit user object regardless of what time it comes
  3. if no user object comes in the first 1000ms, then emit null at the 1000ms mark

Here is what I've done (and it seems to work). However, I'm reluctant to use this code as it doesn't seem that concise:

const o1 = observable.pipe(skipUntil(timer(1000)))

const o2 = observable.pipe(
    takeUntil(o1),
    filter((user) => user !== null)
)

const o3 = timer(1000).pipe(takeUntil(o2), mapTo(null))

merge(o1, o2, o3).subscribe({
    next: setUser,
    error: console.log,
    complete: () => console.log("error: obs completed, this shouldn't happen"),
})

Is there a way to do this without merge? I tried going through the docs but I'm quite lost.

Thanks for your help!

2 Answers

You could use concat instead of merge. Think of it as using the first source until it completes, then use the second source.

const nonNullUser = firebaseUser.pipe(
    filter(user => user !== null),
    takeUntil(timer(1000))
);

const user = concat(nonNullUser, firebaseUser);

user.subscribe(...);

I just realized that this solution will not explicitly perform step #3 "emit null at the 1000ms mark". I was thinking subscribing to firebaseUser would emit the latest value. But, I'm not sure if that's true for your scenario.

If not, we could easily achieve this by adding shareReplay like this:

const firebaseUser = observable.pipe(shareReplay(1));

While I liked the answer from @BizzyBob I was genuinely intrigued by these requirements that I wanted to see what other options were available. Here's what I produced:

const auth$ = observable.pipe(
    startWith(null)
)

const null$ = timer(1000).pipe(
    switchMap(_=>auth$)
)

const valid$ = auth$.pipe(
    filter(user=>!!user)
)

const user$ = race(null$, valid$);

We have our source auth$ observable which gets your Firebase data. However, startWith() will immediately emit null before any values coming from Firebase.

I declared two observables for null and non-null cases, null$ and valid$.

The null$ observable will subscribe to auth$ after 1000ms. When this happens it immediately emits null thanks to the startWith() operator.

The valid$ observable subscribes to auth$ immediately but only emits valid user data thanks to filter(). It won't emit startWith(null) because it is caught by the filter.

Last, we declare user$ by using the race() operator. This operator accepts a list of observables as its parameters. The first observable to emit a value wins and is the resulting subscription.

So in our race, valid$ has 1000ms to emit a valid user. If it doesn't, race() will subscribe to null$ resulting in the immediate null, and all future values coming from Firebase.

Related