3

Exploring observables, I've run into a problem where I need to execute functions only after another function has been completed. All functions are async operations that use the Http service in @angular/http.

The call chain should be

  1. Check if playlist exists. If playlist doesn't exist, create it.
  2. GET recommended tracks
  3. Add recommended tracks to playlist

Normally, I would've written this with promises and have a chain that would look something like

this.checkIfPlaylistExists()
    .then((exists) => {
        if(!exists) {
            return this.createPlaylist();
        }
    })
    .then(() => this.getRecommendedTracks(seedTracks)) //resolves tracks
    .then(this.addRecommendedTracksToPlaylist)
    .then(doSomethingElse)
    .catch();

My confusion is how I would write the code using observables instead. What I have so far isn't working 100%, and looks like this

  createRecommendedTracksPlaylist(seedTracks: Track[]) {
    this.checkIfPlaylistExists()
      .map(exists => {
        if (!exists) {
          return this.createPlaylist();
        }
      })
      .map(() => this.getRecommendedTracks(seedTracks))
      .map((tracks) => this.addRecommendedTracksToPlaylist(tracks)) //<--- if I comment out this line, 'getRecommendedTracks' is executed and prints out response
      .mergeAll()
      .subscribe();
  }

The functions called

getRecommendedTracks(seedTracks: Track[]) {
    seedTrackIds = seedTracks.map((track) => track.id).join(',');
    const recommendationsUrl = `https://api.spotify.com/v1/recommendations?seed_tracks=${seedTrackIds}`;
    return this.http.get(recommendationsUrl, options).map((res) => {
      const recommendedTracks = res.json().tracks as Array<Track>;
      console.log('Getting recommended tracks'); //Is only run if I don't run 'addRecommendedTracksToPlaylist' function
      console.log(recommendedTracks);
      return recommendedTracks;
    });
}

addRecommendedTracksToPlaylist(tracksToAdd) {
    console.log('Going to add tracks..');
    console.log(tracksToAdd); //Prints out 'Observable {_isScalar: false, ... }'
    const options = this.getAuthHeader();
    const playlistUri = '2OGx5l1ItjzMsQdQ0Hec6g';
    const addTracksUrl = `https://api.spotify.com/v1/users/${this.userId}/playlists/${playlistUri}/tracks`;
    return this.http.get(addTracksUrl, options);
}

Edit: Adding the checkIfPlaylistExists code (please not that the playlist currently always exists)

  private checkIfPlaylistExists() {
    const options = this.getAuthHeader();
    const playlistUrl = `https://api.spotify.com/v1/users/${this.userId}/playlists`;
    return this.http.get(playlistUrl, options).map((res) => {
      const playlists = res.json().items as Array<any>;
      return playlists.some((e, i, a) => {
        return e.name.indexOf(this.playlistTitle) > -1;
      })
    });
  }

So to sum it up

  • How would one write a sequential execution order like this using observables?
  • Why is the function getRecommendedTracks only executed if I don't let the function addRecommendedTracksToPlaylist run in the 'chain'?
  • Have I completely misunderstood the Observable.map function? All examples I've looked at use it for single objects as well as arrays, but I'm not sure I'm using it correctly.
Daniel B
  • 8,770
  • 5
  • 43
  • 76
  • .switchMap() is your friend – Jesse Carter Jul 11 '17 at 17:16
  • Possible duplicate of [Angular 2: Two backend service calls on success of first service](https://stackoverflow.com/questions/36712659/angular-2-two-backend-service-calls-on-success-of-first-service) – eko Jul 11 '17 at 17:16

1 Answers1

2

Just change your .map() to.flatMap() and you are good to go:

createRecommendedTracksPlaylist(seedTracks: Track[]) {
    this.checkIfPlaylistExists()
        .flatMap(exists => {
            if (!exists) {
                return this.createPlaylist();
            }
            return Observable.of(1);
        })
        .flatMap(() => this.getRecommendedTracks(seedTracks))
        .flatMap((tracks) => this.addRecommendedTracksToPlaylist(tracks))
        .subscribe();
}

You will need to return a dummy Observable if the playlist doesnt exist, because flatMap expect a return of Observable. Observable.empty() won't work because it just end the stream.

CozyAzure
  • 8,280
  • 7
  • 34
  • 52
  • Thanks for your answer! I tried this, but then only the `checkIfPlaylistExists` is executed and nothing more. I added the function to the question in case it's the culprit! – Daniel B Jul 11 '17 at 17:28
  • @DanielB is `this.createPlaylist()` returning an `Observable` as well? – CozyAzure Jul 11 '17 at 17:31
  • Yes, it's returning an `http.post` call, though the playlist always exists in this case! – Daniel B Jul 11 '17 at 17:33
  • @DanielB weird, `flatMap` *is* the equivalent of `.then` in promise – CozyAzure Jul 11 '17 at 17:34
  • I'll keep looking, this will probably be related to some other error! I'll let you know! – Daniel B Jul 11 '17 at 17:35
  • Removing the playlist and letting it be created by the `http.post` solves the problem, but as long as it exists, it doesn't work. I tried `return Observable.empty()` but got the same non-result. – Daniel B Jul 11 '17 at 17:55
  • Without returning anything, the error `You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.` is thrown. – Daniel B Jul 11 '17 at 18:31
  • @DanielB try return a dummy Observable, instead of an empty one. – CozyAzure Jul 12 '17 at 00:15