Be patient for the Observable to finish its execution within another Observable

I have a data structure that consists of two classes: Repo and Contributor.

export class Repo {
    id: number;
    name: string;
    contributors: Contributor[];
}

export class Contributor {
    id: number;
    login: string;
}

Currently, I am using Observable<Repo> to retrieve all repository data. However, I want to ensure that before the outer Repo is emitted fully, an inner observable Observable<Contributor> is called to populate all contributors associated with the repo. I am facing challenges in achieving this and not sure how to proceed. Below is the code snippet I have been working on:

private repos: Repo[] = [];

getRepos(orgName: string): void {
    const repoBuild: Repo[] = [];
    this.githubService.getRepos(orgName).pipe(
        // This part does not wait for all contributors to be resolved
        map(repo => this.getRepoContributors(orgName, repo))
    ).subscribe(
        repo => repoBuild.push(repo),
        error => {
            this.repos = [];
            console.log(error);
        },
        () => this.repos = repoBuild
    );
}

// Function to fetch contributors data for the repo
private getRepoContributors(orgName: string, repo: Repo): Repo {
    const contributors: Contributor[] = [];
    repo.contributors = contributors;
    this.githubService.getRepoContributors(orgName, repo.name)
        .subscribe(
            contributor => {
                // Add the contributors to the collection for this repo
                contributors.push(contributor);
            },
            error => console.log(error),
            () => repo.contributors = contributors
        );
    return repo;
}

As my knowledge on Observable is limited, I have been struggling with this issue for some time now. I have tried searching for solutions on StackOverflow without success. Any assistance or guidance would be highly appreciated!

(The code is implemented in Angular 5)

Solution:

After considering @joh04667's suggestion below, I was able to make it work by implementing the following changes:

getRepos(orgName: string): void {
    const repoBuild: Repo[] = [];
    this.githubService.getRepos(orgName).pipe(
        // Using mergeMap() to replace `repo` with the result from `getRepoContributors`
        mergeMap(repo => this.getRepoContributors(orgName, repo))
    ).subscribe(
        repo => repoBuild.push(repo),
        error => {
            this.repos = [];
            console.log(error);
        },
        () => this.repos = repoBuild
    );
}

// Function to fetch contributors data for the repo
private getRepoContributors(orgName: string, repo: Repo): Observable<Repo> {
    repo.contributors = []; // Ensure each repo has an empty array of contributors
    return this.githubService.getRepoContributors(orgName, repo.name).pipe(
        // Utilizing tap() to inspect each contributor and add them to the array
        tap(contributor => {
            // Add the contributors to the total collection for this repo
            repo.contributors.push(contributor);
        }),
        // Selecting only the last contributor and replacing them with the repo
        last(
            () => false,
            () => repo,
            repo
        )
    );
}

In the final step where last() is used, I inform the Observable that although it will process all values, only the last one will be consumed. Despite being of type Contributor, I replace it with a default value (the repo), which helps transform the return type from Observable<Contributor> to Observable<Repo> as required by the overarching Observable.

Answer №1

This particular question brings to light a crucial aspect of Observables that can really enhance one's understanding: higher-order Observables, also known as Observables that yield other Observables.

If you find yourself in a similar scenario, the mergeMap / flatMap operator could be your go-to solution:

fetchData(id: number): void {
    const dataCollection: Data[] = [];
    this.dataService.fetchData(id).pipe(
        mergeMap(data => this.getDataDetails(id, data))
    ).subscribe(
        updatedData => dataCollection.push(updatedData),
        error => {
            this.dataList = [];
            console.log(error);
        },
        () => this.dataList = dataCollection
    );
}

The mergeMap function essentially maps emitted values from an outer Observable to an inner Observable (getDataDetails) and generates a consolidated Observable stream. It flattens out the transition of values between different Observables, creating a streamlined data flow for subscription.

Although grappling with higher-order Observables may seem daunting at first, it is within these complexities where Observables truly shine. I recommend exploring additional operators like switchMap and concatMap on the provided resource to fully harness the potential of Observables.

EDIT

Upon reassessment, it seems my initial misinterpretation led me to believe that getDataDetails was outputting an Observable. Apologies for the confusion. Here's a revised approach:

By leveraging map for pre-processing values before integration with mergeMap, we can streamline the code significantly:

private dataList: Data[] = [];

fetchData(id: number): void {
    const dataCollection: Data[] = [];
    this.dataService.fetchData(id).pipe(
        map(data => {
            data.details = [];
            return data;
          }),
        mergeMap(data => this.dataService.getDataDetails(id, data.name)),
        map(detail => {
            data.details.push(detail)
          })
    ).subscribe(
        updatedData => dataCollection.push(updatedData),
        error => {
            this.dataList = [];
            console.log(error);
        },
        () => this.dataList = dataCollection
    );
}

// second function no longer required

With strategic utilization of map within the transformation chain, we can seamlessly modify values in a sequential manner as expected by the operator.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Automate the process of replacing imports in Jest automatically

Currently, I am in the process of setting up a testbench for a more intricate application. One challenge we are facing is that our app needs to call backend code which must be mocked to ensure the testbench runs efficiently. To address this issue, we utili ...

The data returned from the API is intact, yet certain fields remain unassigned

When I make a call to an API, I have created a simple model that matches my angular interface exactly. The issue I am facing is that even though I define the variable where I push the data into of that interface type, it still shows the object fields as u ...

What is the correct way to define the interfaces/types in typescript?

I am currently working on setting up an Apollo GraphQL server in Typescript and struggling with understanding the correct approach in dealing with the type system. While GraphQL and Apollo are integral to the code, my main focus is on TypeScript. I am also ...

The functionality of CSS transitions may be affected when dynamically adding a class

Creating a custom CSS for my main div: .main { height: 0%; position: absolute; width: 100%; z-index: 100; background: whitesmoke; bottom: 0; border-radius: 15px; padding: 20px; color: gray; left: 0; right: 0; transition: height 1s e ...

Allusion to a intricate data component

In my code, I have a model that is being represented by a class. For example, let's consider the model of a car: export class Car { public name : string; public color: string; public power : number; public service : boolean; } All c ...

Deactivate the button in the final <td> of a table generated using a loop

I have three different components [Button, AppTable, Contact]. The button component is called with a v-for loop to iterate through other items. I am trying to disable the button within the last item when there is only one generated. Below is the code for ...

In what way can the result of the code displayed be considered as truthful?

this.someService.findDevices() .subscribe((segments) => { this.segments = Array.from(segments.segments); this.packs.forEach((pack) => { pack.segments = Array.from(segments.segments); pack. ...

Trouble retrieving information from my axios fetch response

Can anyone help me identify the issue in my code? I am working on a simple API get request. It successfully retrieves data from my API: const GetMedicalPackages = async (props:IGetMedPack)=>{ const token = props.token const data = axios({ ...

Is it advisable to pass useSelector to useState in React?

Hey everyone, I've got a question about preferences for a specific method: When working with a React functional component in TypeScript that retrieves values from Redux State using useSelector, which approach do you prefer? 1) const campaign = us ...

Utilizing session variables across multiple TypeScript files

There is a session variable called session in my home.component.ts. I need to access this variable in both user.compnent.ts and dashboard.component.ts. This is how the session variable is defined in home.component.ts: var session = sessionStorage.getIte ...

Angular 11 project facing issues with Bootstrap 5 tooltip functionality

Embracing the power of Bootstrap 5.0.2 in an Angular 11 project, I included the following code: index.html <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compa ...

Changing from a string to a personalized data type

I'm a beginner in JavaScript and I've created a custom type called "Stage". My goal is to convert strings to this custom type. The strings will always match one of these values exactly (with the first letter capitalized and written the same way). ...

Linking Angular 2 Production Mode to the vendor directory

I am currently working on an Angular2 project using the angular-cli npm package. To upload the app to the server, I used the following commands: ng build --prod and ng serve --prod. However, after uploading the dist folder to the server and trying to acc ...

Incorporate a New Feature into my NPM Package

I've been searching high and low for an answer to this, but I'm still stuck. I'm working on a module in Angular 2 with ng-module, and everything is functioning properly. However, I'm struggling to assign a property to another property w ...

It is essential for the object to contain a method called '[Symbol.iterator]()' which will yield an iterator upon invocation

Currently, I am facing the following error: error TS2488: Type 'Usuario' must have a '[Symbol.iterator]()' method that returns an iterator. This is my code: usuarios.reducers.ts export interface UsuarioState { users: Usuario[]; ...

Error Encountered: Unable to locate angular/core module in Angular 2

I have encountered an issue while setting up a new Angular2 app from the quickstart folder on the Angular website. Despite following all suggested solutions, I am still facing errors. After running npm install, everything seems fine as I can see my node_mo ...

Polling database from various browser tabs

Working on a .NET/angular application that regularly checks the SQL Server database for updates, with the user able to customize the polling interval starting from 10 seconds (as per business requirements). The issue arises when each new tab opened by a ...

Crafting a Retro Style

I have an interface called Product which includes properties such as name, and I want to track changes to these products using a separate interface called Change. A Change should include the original Product as well as all of its properties prefixed with t ...

Understanding how to use the `e.persist` method in TypeScript can greatly improve

My form validation process using formik includes the code snippet below: {props => { const { values: { email, password }, errors, touched, handleChange, isValid, setFieldTouched, } = props; const change = (name: string, e: ...

Execute multiple observables concurrently, without any delay, for every element within a given list

I've been attempting to utilize mergeMap in order to solve this particular issue, but I'm uncertain if my approach is correct. Within my code, there exists a method named getNumbers(), which makes an HTTP request and returns a list of numbers (O ...