0

I know it is bad practice to call subscribe within subscribe but I don't know how to handle it differently with my special case. The code as it is now works, but my problem is that if I update my website for example every second, parts of the table are loaded first and other parts are loaded afterwards (the content of the subscibe within my subscribe).

I have a service containing a function that returns an Observable of a list of files for different assets. Within that function I request the filelist for each asset by calling another service and this service returns observables. I then iterate over the elements of that list and build up my data structures to return them later on (AssetFilesTableItems). Some files can be zip files and I want to get the contents of those files by subscribing to another service (extractZipService). To be able to get that correct data I need the name of the file which I got by requesting the filelist. I then add some data of the zip contents to my AssetFilesTableItems and return everything at the end.

The code of that function is as follows:

  getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
    const data = assetIds.map(assetId => {
      // for each assetId
      return this.fileService.getFileList(assetId)
        .pipe(
          map((datasets: any) => {
            const result: AssetFilesTableItem[] = [];

            // iterate over each element
            datasets.forEach((element: AssetFilesTableItem) => {

              // apply regex filter to filename
              if (filter.test(element.name)) {
                this.logger.debug(`Filter ${filter} matches for element: ${element.name}`);

                // build up AssetFilesTableItem
                const assetFilesItem: AssetFilesTableItem = {
                  name: element.name,
                  type: element.type,
                  asset: assetId
                };

                // save all keys of AssetFilesTableItem
                const assetFilesItemKeys = Object.keys(assetFilesItem);

                // if file is of type ZIP, extract 'config.json' from it if available
                if (showConfig && element.type.includes('zip')) {
                  this.extractZipService.getJSONfromZip(assetId, element.name, 'config.json')
                    .subscribe((configJson: any) => {
                      const jsonContent = JSON.parse(configJson);
                      const entries = Object.entries(jsonContent);
                      entries.forEach((entry: any) => {
                        const key = entry[0];
                        const value =  entry[1];
                        // only add new keys to AssetFilesTableItem
                        if (!assetFilesItemKeys.includes(key)) {
                          assetFilesItem[key] = value;
                        } else {
                          this.logger.error(`Key '${key}' of config.json is already in use and will not be displayed.`);
                        }
                      });
                    });
                }
                result.push(assetFilesItem);
              }
            });

            return result;
          }));
    });

    // return combined result of each assetId request
    return forkJoin(data);
  }
}

I update my table using the following code within my component:

  getValuesPeriodically(updateInterval: number) {
    this.pollingSubscription = interval(updateInterval)
      .subscribe(() => {
        this.getAssetfilesFromService();
      }
    );
  }

getAssetfilesFromService() {
    this.assetfilesService.getAssetfilesData(this.assetIds, this.filterRegEx, this.showConfig)
    .subscribe((assetFilesTables: any) => {
      this.assetFilesData = [].concat.apply([], assetFilesTables);
    });
  }

Edit: I tried ForkJoin, but as far as I understandit is used for doing more requests in parallel. My extractZipService though depends on results that I get from my fileService. Also I have a forkJoin at the end already which should combine all of my fileList requests for different assets. I don't understand why my view is not loaded at once then.

EDIT: The problem seems to be the subscribe to the extractZipService within the forEach of my fileService subscribe. It seems to finish after the fileService Subscribe. I tried lots of things already, like SwitchMap, mergeMap and the solution suggested here, but no luck. I'm sure it's possible to make it work somehow but I'm running out of ideas. Any help would be appreciated!

Manuela
  • 127
  • 1
  • 15
  • look at `forkJoin` in accepted answer to run requests in parallel – AT82 Aug 01 '19 at 10:24
  • As far as I understand, forkJoin is used for doing more requests in parallel, but my extractZipService depends on results that I get from my fileService. Also I have a forkJoin at the end already which sould combine all of my fileList requests for different assets. I don't understand why my view is not loaded at once then. – Manuela Aug 01 '19 at 10:31
  • okay, was a bit hasty then and didn't read properly. Sorry, my fault. I reopened question :) – AT82 Aug 01 '19 at 10:33

1 Answers1

1

You are calling this.extractZipService.getJSON inside a for loop. So this method gets called asynch and your function inside map is not waiting for the results. When result does come as your items are same which is in your view they get refreshed.

To solve this you need to return from this.extractZipService.getJSON and map the results which will give you a collections of results and then you do forkJoin on results ( Not sure why you need to forkjoin as there are just the objects and not API's which you need to call )

this.logger.debug(`ConfigJson found for file '${element.name}': ${configJson}`);
const jsonContent = JSON.parse(configJson);
const entries = Object.entries(jsonContent);
entries.forEach((entry: any) => {
 // code
});

complete code should look on similar lines :-

getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
    const data = assetIds.map(assetId => {
      // for each assetId
      return this.fileService.getFileList(assetId)
        .pipe(
          map((datasets: any) => {

            // iterate over each element
            datasets.forEach((element: AssetFilesTableItem) => {

              return this.extractZipService.getJSONfromZip(assetId, element.name, 
              'config.json')
            });               
           })).map((configJson: any) => {
              // collect your results and return from here        
              // return result
         });;    
     });

    // return combined result of each assetId request
    return forkJoin(data);
  }
}

I have created a Stackblitz(https://stackblitz.com/edit/nested-subscribe-solution) which work along the same lines. You need to use concatMap and forkJoin for getting all the results. Hope this helps.

Sanket
  • 612
  • 5
  • 23
  • Thanks, I know now what the problem is, but I'm still stuck on solving it. I tried your solution but it is not working as I call the extractZipService only when file type is zip and sometimes the zip service will return data, sometimes it won't (when there is no config.json within that zip file). That's intended behavior. Actually, the getJSONFromZip method returns an Observable. Maybe there is another way to make the function inside map wait for the results of the ZipService. I think I'll have to dive a bit deeper into rxjs on monday.. – Manuela Aug 02 '19 at 09:56
  • @Manuela Created a Stackblitz. Hope this helps. – Sanket Aug 06 '19 at 07:10