RXJS wait untill all observables in a child object resolve

Viewed 256

I have a project where I make an API request and create an object by mapping data to a model (Parent). The object I create exists of a nested structure (Children) of objects. For each object a new API call needs to be made.

When I call my service that does all the above, I want to have one observable returned on which I can subscribe, but because the child structure is dynamic I cannot figure out how to get those requests combine.

// Parent
export interface Parent {
    children: Child[];
}

// Child
export interface Child {
    id: string;
    infoFromApi: ApiInfo
    children: Child[]
}

// Service
getAPIResponse(endpoint: string): Observable<Parent> {
    return this.http.get(endpoint)
        .pipe(
            take(1),
            catchError((error: HttpErrorResponse) => this.handleError(error, messageGroup)),
            map((parent: RawParent) => {
                return this.enrichChildren(parent.children);
            })
        );
}

enrichChildren(children: Child[]){
    children.forEach((child) => {
        child.infoFromApi = return this.http.get(endpoint);
        this.enrichChildren(child.children);
    })
}

This is a simplified mockup of my code please do not look at spelling mistakes.

1 Answers

Here is a partial solution to your problem.

The important part of the snippet below is inside the enrichChildren so what is happening inside, first I'm creating a Subject that will notify all interested observers when all children had been fetched, after that i create an empty array that will hold children related data while the requests are being processed.

After that, I create the recursiveCaller that will do the magic. If you look inside the function, I am creating a stream from the input elements x with the help of the from operator (in this example the elements are id-s) which mean that for each element inside the incoming collection of id-s there will be an emission from the source.

After that with the help of mergeMap I'm initiating a call for each id from the passed children, also combining them in a single observable.

On the next line after I receive the response from each request depending on the count of children inside the response I either pass the response down the stream or pass it together with the response that i will get for the element's children.

After that in the subscription whenever, I get some data I push it to my populatedStructure variable, for later usage.

At some point when all calls are complete (the angular http client auto-completes observables that are related to http requests, so you can also skip the take(1)) inside the complete cb in the subscription I trigger the next of my emitter subject which will notify the subscriber outside the function that the children had been enriched.

import { of, from, concat, Subject, merge } from "rxjs";
import { concatMap, delay, mergeMap, switchMap } from "rxjs/operators";

const fetcher = mockFetcher();

function enrichChildren(x) {
  const emitter = new Subject();
  const populatedStructure = [];
  function recursiveCaller(elements: [any]) {
    return from(elements).pipe(
      mergeMap(x => {
        return fetcher.getById(x);
      }),
      mergeMap(response => {
        if (response.children) {
          return merge(of(response), recursiveCaller(response.children));
        } else {
          return of(response);
        }
      })
    );
  }

  recursiveCaller(x).subscribe(
    x => {
      console.log(x);
      populatedStructure.push(x);
    },
    () => {},
    () => {
      emitter.next(populatedStructure);
    }
  );
  return emitter.pipe(take(1));

}
// Id of the root element
enrichChildren([0]).subscribe(x => console.log(x));


function mockFetcher() {
  const db = {
    "0": {
      id: "0",
      children: ["1", "2"]
    },
    "1": {
      id: "1",
      children: ["1_1", "1_2"]
    },
    "1_1": {
      id: "1_1",
      children: []
    },
    "1_2": {
      id: "1_2",
      children: ["1_2_1"]
    },
    "1_2_1": {
      id: "1_2_1",
      children: ["1_2_1_1"]
    },
    "1_2_1_1": {
      id: "1_2_1_1",
      children: []
    },
    "2": {
      id: "2",
      children: []
    }
  };

  function getById(id) {
    return of(db[id]).pipe(delay(1000));
  }

  return {
    getById
  };
}

Here is a link to a working solution : stackblitz

Related