skip to Main Content

I’m fairly new to angular and javascript and building my first angular dashboard app that queries the Azure Devops api to get test results:

  1. Query the API to get a filtered list of release definitions (about 100)
  2. For each release definition get the latest release
  3. For each release get a collection of all the test runs from this release.
  4. Show a table of all results (each release definition is a row with the test results as expandable table) as soon as the first set of test results are received.

I managed to get this working with nested subscriptions on observables (see below), but I understand this should be avoided and is better done with something likt mergeMap / switchMap and/or forkJoin. Been struggling with that for days, but no luck yet.

And then there’s challenge #2: a second stream of data should be added to this: pipelines. Following the same recipe: get a list of pipelines, for each the latest pipeline run, for each of that all the test runs. Both data streams can/should be obtained separately and asynchronously and as soon as one of them has fetched the first set of test runs it can be shown on the dashboard.

How to accomplish this??

My working solution for release definitions only using nested subscriptions:

ngOnInit(): void {   
    this.router.paramMap.pipe(takeUntil(this.ngUnsubscribe))
    .subscribe(params => {
      this.teamToFilterOn = params.get('team');
      this.apiService.getReleaseDefinitions(this.teamToFilterOn as string)
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe((releaseDefinitions: any)=> {
        if (releaseDefinitions.length === 0) {
          this.isLoading = false
        }
        releaseDefinitions.forEach((releaseDefinition: any) => {
          if (releaseDefinition.lastRelease) {
            this.apiService.getRelease(releaseDefinition.lastRelease.id)
            .pipe(takeUntil(this.ngUnsubscribe))
            .subscribe((info: PipelineOrReleaseInfo) => {
              if (info) {
                this.apiService.getTestRunsByRelease(info.releaseId)
                .pipe(takeUntil(this.ngUnsubscribe))
                .subscribe((testruns: any) => {    
                  this.isLoading = false;              
                  this.results = [...this.results, { info: info, testruns: testruns, totals: this.calculateEnvironmentTotals(testruns.testRunResults)}];             
                  this.dataSource.data = this.results;
                });
              }              
            });
          }
        });            
      });
    });
  }   

First try using forkJoin but stuck on how to proceed. Also not sure if this is correct, because forkJoin seems to wait until both observables are complete, but instead as soon as one of them has a result it should proceed to loop over the results and do the remaining calls.

ngOnInit(): void {   
    this.router.paramMap.pipe(takeUntil(this.ngUnsubscribe))
    .subscribe(params => {
      this.teamToFilterOn = params.get('team');      
      
      let releaseDefQuery =  this.apiService.getReleaseDefinitions(this.teamToFilterOn as string)
      let pipelineDefQuery = this.apiService.getPipelineDefinitions(this.teamToFilterOn as string)

      forkJoin([releaseDefQuery, pipelineDefQuery]).subscribe(definitions => {
        let releaseDefinitions = definitions[0];
        let pipelineDefinitions = definitions[1];

        releaseDefinitions.forEach((releaseDefinition: any) => {
          if (releaseDefinition.lastRelease) {
            this.apiService.getRelease(releaseDefinition.lastRelease.id)
            .pipe(takeUntil(this.ngUnsubscribe))
            .subscribe((info: PipelineOrReleaseInfo) => {
...

2

Answers


  1. Like any kind of Stream (e.g. Promises), when you see nesting in Observables you might want to take a step back to see if it’s really warranted.

    Let’s examine your solution bit by bit.

    Our starting point is:

    this.router.paramMap.pipe(takeUntil(this.ngUnsubscribe))
    

    Then you subscribe, but within that subscribe you do observable operations on the given data, this strongly suggests you should be pipeing an operation instead, and then subscribe on the final result.

    In this case you want to map your params to some Observable. You also might benefit from the "interrupt early" behavior that switchMap offers. Otherwise there’s also mergeMap as a potential option if you don’t want to "interrupt early" (it used to be more appropriately named flatMap).

    We’ll add a filter and map for good measure, to ensure we have the team param, and to pluck it out (since we don’t need the rest).

    this.router.paramMap.pipe(
        takeUntil(this.ngUnsubscribe),
        filter(params => params.has("team"))
        map(params => params.get("team"))
        switchMap(team => {
          this.teamToFilterOn = team as string;
          // We'll dissect the rest
        })
    ) // [...]
    

    Then comes the part with what you want to do with that team.

    You have multiple "tasks" that rely on the same input, and you want them both at the same time, so reaching for forkJoin is a good call. But there’s also combineLatest that does something similar, but combine the results "step by step" instead.

    You use the word "latest" for both your tasks, so we’ll indeed reach for combineLatest instead:

    const releaseDef$ = // [...]
    const pipelineDef$ = // [...]
    return combineLatest([releaseDef$, pipelineDef$]);
    

    Now let’s dissect these two operations.

    From what I gather, you’re only interested in releases that have a lastRelease. You also don’t want to "switch" when a new one comes in, you want them all, let’s encode that:

    const releaseDef$ = this.apiService.getReleaseDefinitions(this.teamToFilterOn as string).pipe(
        filter(releaseDef => releaseDef.lastRelease),
        mergeMap(lastReleaseDef => this.apiService.getRelease(releaseDefinition.lastRelease.id)),
        filter(info => !!info)
        mergeMap(info => this.apiService.getTestRunsByRelease(info.releaseId))
        tap(testruns => {
          this.isLoading = false;              
          this.results = [...this.results, { info: info, testruns: testruns, totals: this.calculateEnvironmentTotals(testruns.testRunResults)}];             
          this.dataSource.data = this.results;
        })
    )
    

    Here I’ll use tap assuming you operate independently on the results of releaseDef$ and pipelineDef$. If you don’t, skip the taps and pass a callback to the final subscribe instead.

    To wrap this up, let’s put it all together:

    ngOnInit(): void {
      this.router.paramMap.pipe(
        takeUntil(this.ngUnsubscribe),
        filter(params => params.has("team"))
        map(params => params.get("team"))
        switchMap(team => {
          this.teamToFilterOn = team as string;
          
          const releaseDef$ = this.apiService.getReleaseDefinitions(this.teamToFilterOn as string).pipe(
            filter(releaseDef => releaseDef.lastRelease),
            mergeMap(lastReleaseDef => this.apiService.getRelease(releaseDefinition.lastRelease.id)),
            filter(info => !!info)
            mergeMap(info => this.apiService.getTestRunsByRelease(info.releaseId))
            tap(testruns => {
              this.isLoading = false;              
              this.results = [...this.results, { info: info, testruns: testruns, totals: this.calculateEnvironmentTotals(testruns.testRunResults)}];             
              this.dataSource.data = this.results;
            })
          );
    
          const pipelineDef$ = // You didn't present code for this, but you get the idea
          return combineLatest([releaseDef$, pipelineDef$]);
        })
      ).subscribe(([release, pipeline]) => {
        /* add your logic here if you don't use `tap`s */
      });
    }
    

    You’ll notice I only used on takeUntil(this.ngUnsubscribe) as the "main" observable chain will stop with that, which means operation will stop as well.

    If you’re unsure or encounter issues, you can still sprinkle them as the very first argument of each .pipe.

    Login or Signup to reply.
  2. How to conditionally merge multiple observable list using switchMap

    $data = this.catService.get().pipe(
        switchMap((cats) => {
          return this.nestService.get().pipe(
            map((nests) => {
              return cats.map((cat) => {
                return {
                  ...cat,
                  nest: nests.find((nest) => (nest.catid = cat.id)),
                };
              });
            })
          );
        })
      );
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search