Using Async/Await to recursively traverse a tree in TypeScript

Here is the basic tree structure I am working with:

class Group {
  id: ObjectId;
  name: string;
  subGroups: [ObjectId]
}

https://i.sstatic.net/woZFs.jpg

For instance, Group A contains Group B and C as subGroups, while Group C has subGroups F, G, H, and so on.

I am in need of implementing an algorithm to recursively fetch all groups:

Expected Output = [Group A, Group B, Group C, Group D, Group E, Group F, Group G, Group H, Group I, Group J]

However, I require fetching the subGroups asynchronously from the database using async/await.

Approach 1

    const tr = async (group: Group, result: Group[]) => {
      console.log(group);
      result.push(group);
      for (const id of group.subGroups) {
        const groupId = id.toHexString();
        const subGroup = this.findGroup(groupId);
        tr(await subGroup, result);
      }
    };

    const result = [];
    await tr(user.group, result);
    console.log(result);

Approach 2

  async transverse(group: Group, result: Group[]) {
    console.log(group);
    result.push(group);
    for (const id of group.subGroups) {
      const groupId = id.toHexString();
      const subGroup = await this.findGroup(groupId);
      await this.transverse(subGroup, result);
    }
  }

  const result = [];
  await transverse(user.group, result);
  console.log(result);

Approach 1 fails to provide the correct array output and does not include Groups A to J. Approach 2 achieves the desired array, but the code lacks cleanliness. Can someone suggest an elegant solution to accomplish this task and explain why approach 1 falls short?

Answer №1

If you want to enhance your tree traversal process, consider incorporating modern features such as an async generator.

Given that the example demonstrates a breadth-first traversal, opting for iteration instead of recursion would be more suitable. By looping through each level of the tree, Group objects on the same level can be resolved in "parallel" since their outcomes are independent of each other. This scenario is perfect for utilizing Promise.all rather than using individual awaits for each node separately.

Let's visualize how this implementation could appear by including a simulated database component:

class Group {
    constructor(id, subGroups) {
        this.id = id;
        this.subGroups = subGroups;
    }
}

// Simulated asynchronous DB operations
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const db = {
    _tree: {
        "A": ["B", "C"],
        "B": ["D", "E"],
        "C": ["F", "G", "H"],
        "D": [],
        "E": [],
        "F": ["I", "J"],
        "G": [],
        "H": [],
        "I": [],
        "J": []
    },
    async findGroup(groupId) {
        await delay(Math.random() * 1000);
        return new Group(groupId, db._tree[groupId]);
    }
};

// Establish an async generator function
async function* tr(groupId) {
    let result = [await db.findGroup(groupId)];
    while (result.length) {
        yield* result;
        // Follow a Breadth-first traversal approach and introduce some parallelism
        result = await Promise.all(result.flatMap(group => group.subGroups.map(db.findGroup)));
    }
};

// Consume the results from the async iterator generated by the above function. Begin by passing the ID of the root of the tree:
(async () => {
    for await (const result of tr("A")) {
        console.log(result);
    }
})();

Answer №2

Alright, let's take a look at your class structure

class Group {
  id: ObjectId;
  name: string;
  subGroups: [ObjectId]
} 

We must clarify our requirements so, The anticipated outcome should be:

[Group A, Group B, Group C, Group D, Group E, Group F, Group G, Group H, Group I, Group J

In essence, we need to iterate in the following manner: group -> all children and repeat.

We can utilize a variable to store the upcoming groups and then iterate over them. This variable will be initialized with the first group that is fetched (For instance, group A) and subsequently the loop will display its name, iterate through its children, and preserve each child to be next in the print-to variable we created earlier. Once we finish iterating through its children, we will eliminate the primary group.

We will incorporate a while loop that will only terminate when the nextToPrint variable is an empty array.

nextToPrint = []; // variable to hold the next groups to print so it will need to be initialized with groupA.


// some method that runs when your app starts, like mounted() on Vue, attached() on aureliaJS or ngOnInit on Angular
async someMethod() {
  const first = await getGroup(first) // obtain the first group
  nextToPrint.push(first); // add to the queue-like variable


  // iterate through the next to print 
  while(nextToPrint.length) {
      printGroup(nextToPrint[0]); // display the first item in the next list
  }
}

async printGroup(group: Group) {
   console.log(group.name); // display Group name
   group.subGroups.forEach(g => {
       const child = await getGroup(g); // retrieve the next group
       nextToPrint.push(child); // include child to be the last one to print
       console.log(child.name); // show the child name
   });

   nextToPrint.shift(); // remove the first element from the next to print
}

I hope this explanation assists you. Please don't hesitate to leave comments or ask additional questions until we achieve the desired outcome

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

Using Formik with MUI Multiple Select

Exploring the world of MUI and Formik, I encountered a challenge with creating a multiple Select in my form. It's my first time working with these tools, so I have a question regarding the implementation. Here's what I've tried based on the ...

I need to save the API response in object form from Angular so that I can use it later. Can someone suggest the best way to store this response efficiently?

Angular: I need to find a way to save the API response, which is in object format, for future use and to reduce additional API requests. Can someone suggest a convenient method to handle this? Snippet of My Code (Service File): getList(){ .... .... ...

Can data be transferred using ajax upon clicking a button and then utilizing the data in a form on the same page?

Is there a way to send data through ajax when a button is clicked and then use that value in a form on the same page? I have multiple buttons, each with a unique data attribute. I want the value of the clicked button to be sent via ajax and used within a ...

The function useNuxtApp() in Nuxt 3 is returning an unknown type

I have been working on creating a helper that can be used across all composables and applications in my Nuxt plugin. Here is how the code looks: // hello.ts export default defineNuxtPlugin(async nuxtApp => { nuxtApp.vueApp.provide('hello', ...

Is it possible to use both parameters and callback functions in the MongoDB update() function in tandem?

I am looking to create a custom callback function that will check the success of the update() process and return a Q promise based on the outcome. var myCustomFunction = function (name, email) { var deferred = Q.defer(); MongoClient.connect(mongodbU ...

Locate the final element within an array using JavaScript

Provided with a file path like new/lib/java.exe, I am looking to eliminate the root folder 'new' and structure the new path as lib/java.exe. Challenge: After my attempts, I am left with the path as lib/java.exe/ which includes an unwanted "/". I ...

Why is it that when a boolean value is logged as a checkbox, it shows up as undefined?

In my code, I'm attempting to log the value of a variable named check, which corresponds to column 11 in a spreadsheet. This variable is meant to represent the state of a checkbox (true or false) based on whether it's been ticked or not. However, ...

TranslateY animation glitching

I need help with expanding a box to 100% height after click, then collapsing and sticking to the bottom. I created a function in Vue framework to handle the animation, but it's not working smoothly. How can I make it less buggy? Check out the demo her ...

Utilizing TypeScript interfaces with additional parameter object members does not result in the anticipated compilation error

Consider the different types listed below: type Person = { id: string; name: string; }; interface PeopleRepository { getPerson(query: { id: string }): Person; } class Repository implements PeopleRepository { getPerson({ id, age }: { id: string; ...

Modifying a variable within an arrow function does not result in the variable being changed when checked outside of the arrow

I am currently developing an application with Angular and Typescript where I need to update the value of a variable inside a function. To retrieve the data required, I'm utilizing a service. Below is the code snippet for reference: let isDataAvailab ...

Run numerous React applications simultaneously for extended periods of time

Seeking guidance on improving my current process. I am a ReactJs tutor who needs to review apps created by my students. Initially, I would clone the repository onto my PC, install node_modules, run the app, then delete the folder before moving on to the ne ...

Applying the jqtransform plugin to all forms except for one

We've implemented the jQuery jqtransform plugin to enhance the appearance of our website's forms. However, there is one particular form that we'd like to exclude from this transformation. I made adjustments to the script that applies jqtrans ...

Issue: "Stumbled upon an unknown provider! This often implies the presence of circular dependencies"

Looking for help on a perplexing error in my Angular / TypeScript app. While we wait for an improved error message, what steps can we take to address this issue? What are the potential triggers for this error to occur? Uncaught Error: Encountered undefi ...

React element failing to appear on webpage

I'm having trouble loading the snippetInfo.js file in the HTML, and I can't seem to figure out why. I've searched online extensively for solutions, but none of them have worked for me. Whenever I try adding type='text/javascript' t ...

What is the best way to ensure that the buttons remain in place once they have been clicked to reveal a drop-down menu?

Is there a way to keep 3 buttons inline and prevent them from moving when clicked to open a submenu? Changing their positions results in them stacking on top of each other. Any help or suggestions would be greatly appreciated, thank you! Here is the code ...

Tips for implementing a "load more" button to fetch the next 50 requests using axios

Below is the current code I am using, with the start of the URL within my proxy: import React, { Component } from "react"; import axios from "axios"; class BeerList extends Component { state = { beers: [] }; componentDidMount() { axios ...

JavaScript fails to effectively validate URLs

I have implemented the following validation for URLs: jQuery.validator.addMethod("urlValidatorJS", function (value, element) { var regExp = (/^HTTP|HTTP|http(s)?:\/\/(www\.)?[A-Za-z0-9]+([\-\.]{1}[A-Za-z0-9]+)*\.[A-Za-z]{ ...

Error messages are displayed when using Express, while Promise.allSettled successfully completes its tasks

Currently, I am utilizing json-server to establish a basic api server. This project serves as a practical exercise for me to enhance my comprehension of async/await and asynchronous coding in javascript. Displayed below is the code I utilize to initiate j ...

javascript functionality may be affected in Internet Explorer and Firefox following the upgrade to jquery version 1.8.3

After upgrading to jquery 1.8.3 from jquery 1.7 (where everything was functioning properly in all browsers), I added a javascript plugin that specifically requires jquery 1.8.2 or 1.8.3 Unfortunately, the image resizing functionality of this javascript is ...

The error "marker.setMap not a function" was not caught in the Google Maps API

Currently, I am creating a traffic jam application using MeteorJS 1.4.4.2 + ReactJS. Previously, I have utilized the atmosphere google map package and two popular Google Map npm packages (istarkov and tomchentw), which worked well but did not offer the spe ...