1

I am having trouble combining the emitted values of two different Observables for use in an http request, and then returning the request's Observable for use by clients.

Some background: I am working on a Twitch extension. Part of their ecosystem is that extensions receive environment information through event callbacks. The ones I am interested in are located at window.Twitch.ext.onAuthorized() and window.Twitch.ext.configuration.onChanged() (if interested, see here for more details: https://dev.twitch.tv/docs/extensions/reference#helper-extensions).

When making calls to my backend, I need information from both of the above events. These will not change often (if ever), but I can't make calls until they are both available, and I want to get the most recently provided value when making calls. It looks like BehaviorSubjects would be ideal for this:

export class TwitchService {
  private authSubject: BehaviorSubject<TwitchAuth> = new BehaviorSubject<TwitchAuth>(null);
  private configSubject: BehaviorSubject<TwitchConfig> = new BehaviorSubject<TwitchConfig>(null);
  private helper: any;

  constructor(private window: Window) {
    this.helper = this.window['Twitch'].ext;
    this.helper.onAuthorized((auth: TwitchAuth) => {
      this.authSubject.next(auth);
    });
    this.helper.configuration.onChanged(() => {
      this.configSubject.next(JSON.parse(this.helper.configuration.global.content));
    });
  }

  onAuthorized(): Observable<TwitchAuth> {
    return this.authSubject.asObservable().pipe(filter((auth) => !!auth));
  }

  onConfig(): Observable<TwitchConfig> {
    return this.configSubject.asObservable().pipe(filter((config) => !!config));
  }
}

This model works well for parts of the app that subscribe to one of those two Observables. My problem is I cannot find a working way to combine them and use the latest emitted values from both to create a single-use Observable for http requests.

Here's what I have so far:

type twitchStateToObservable<T> = (auth: TwitchAuth, config: TwitchConfig) => Observable<T>;

export class BackendService {
  constructor(private http: HttpClient, private twitch: TwitchService) {}

  private httpWithTwitchState<T>(f: twitchStateToObservable<T>): Observable<T> {
    let subject = new Subject<T>();
    combineLatest(this.twitch.onAuthorized(), this.twitch.onConfig()).pipe(
      first(), // because we only want to make the http request one time
      map(([auth, config]) => f(auth, config).subscribe((resp) => subject.next(resp)))
    );
    return subject.asObservable();
  }

  exampleHttpRequest(): Observable<Response> {
    return this.httpWithTwitchState((auth, config) =>
      // this.url() and this.headers() are private functions of this service
      this.http.get<Response>(this.url(config) + 'exampleHttpRequest', { headers: this.headers(auth, config)})
    );
  }
}

And then, a client of this service should be able to make http requests with this simple call, without needing to know anything about the events or care when they fire:

this.backend.exampleHttpRequest().subscribe((resp) => {
  // do stuff with resp here...but this is never hit
}

Based on my understanding, combineLatest() should emit new values whenever either of the input Observables emits a new value. However, the call f(auth, config) inside of map() is never triggered in my application. I've tested it with breakpoints, with console.logs, and by keeping an eye on the Network tab in the browser debugger tools. It's possible the call to first() is throwing it off, but I don't want to repeat the http request if the events fire again, for obvious reasons.

Thanks in advance for any advice or pointers!

lettucemode
  • 422
  • 4
  • 14
  • Despite some non-idiomatic usage of RxJS, it looks like it should work. Are you sure that `helper.onAuthorized` and `helper.configuration.onChanged` callbacks are actually triggered in `TwitchService`. I can imagine a case where helper has emitted its events before TwitchService has been instantiated. – amakhrov Aug 03 '20 at 20:49
  • @amakhrov good idea - I tested it out with some breakpoints. `TwitchService` constructor is hit first, then `httpWithTwitchState()` is hit (because another component is making an http call in its `ngOnInit()`), then both of the events fire in `TwitchService`. That matches up with my expected order of events. Do you have anything I could read about idiomatic usage of RxJS? Maybe if I clean that up the problem will fix itself. – lettucemode Aug 04 '20 at 02:51

2 Answers2

1

You can think of concat like a line at a ATM, the next transaction (subscription) cannot start until the previous completes!

Example : https://stackblitz.com/edit/typescript-3qsfgk?file=index.ts&devtoolsheight=100

Sample:

const mergeValue = concat(authSubject,configSubject) 

mergeValue.subscribe( (x) => {
  // do stuff with resp here
});
karthicvel
  • 343
  • 2
  • 5
  • Hey, thanks for your answer - but I don't think `concat` fits my use case here. I'm trying to coordinate the output of two events that I don't have control over when they fire, and do an http call only after they have both fired, using the data from each. – lettucemode Aug 04 '20 at 02:53
1

Ok, it turns out I was missing something obvious. I was not subscribing to the combineLatest() call, so it was never being executed. After adding a subscribe() at the end, this worked fine:

private httpWithTwitchState<T>(f: twitchStateToObservable<T>): Observable<T> {
  let subject = new Subject<T>();
  combineLatest(this.twitch.onAuthorized(), this.twitch.onConfig()).pipe(
      first(),
      map(([auth, config]) => f(auth, config).subscribe((resp) => subject.next(resp)))
  ).subscribe(); // adding this fixed the problem
  return subject.asObservable();
} 

After doing that, I did a little more research about how to un-nest subscriptions in cases like this, which led me to the flatMap() operator. So now the httpWithTwitchState() function looks like this:

private httpWithTwitchState<T>(f: twitchStateToObservable<T>): Observable<T> {
  return combineLatest(this.twitch.onAuthorized(), this.twitch.onConfig()).pipe(
    first(),
    map(([auth, config]) => f(auth, config)),
    flatMap((resp) => resp)
  );
}

This works as expected now and is cleaner. Thanks to those who took the time to read my question and/or reply.

lettucemode
  • 422
  • 4
  • 14
  • 1
    Good catch! Btw, That's a part of the "non-idiomatic rxjs code" :). There is no need to create a new subject inside httpWithTwitchState. And furthermore, no need to subscribe inside map() operator (which is actually bad - `map` is designed for data transformation and not for side effects. – amakhrov Aug 04 '20 at 08:12
  • 1
    Instead, this method would look like: `return combineLatest([onAuth, onConfig]).pipe(first(), switchMap(([auth, conf]) => f(auth, conf)))` – amakhrov Aug 04 '20 at 08:14