using pipe async always stucks on loading

485 Views Asked by At

I'm joining multiple observables in one global joined$ observable using switchMap and combineLatest

this is the component TS file:

export class ProgressComponent implements OnInit{

    user: User;
    joined$: Observable<any>;

    constructor(
        protected tasksService: TasksService,
        protected courseService: CoursesService,
        protected fieldService: FieldsService,
        protected sessionService: SessionsService,
        protected applicationService: ApplicationForSessionsService,
        protected TaskdatesService: SessionTaskDatesService,
        protected router: Router,
        private userStore: UserStore)
    {}

    ngOnInit(): void {
        this.user = this.userStore.getUser();
        this.joined$ = this.applicationService.list(1, 10, undefined, this.user.id)
            .pipe(
                switchMap(applications => {
                    const courseSessionIds = uniq(applications.map(application => application.courseSessionId))

                    return combineLatest(
                        of(applications),
                        combineLatest(
                            courseSessionIds.map(courseSessionId => this.sessionService.get(courseSessionId).pipe(
                                switchMap(session => {
                                    const courseId = session.courseId
                                    return combineLatest(
                                        of(session),
                                        combineLatest(
                                            this.courseService.get(courseId).pipe(
                                                switchMap(course => {
                                                    const fieldId = course.fieldId

                                                    return combineLatest(
                                                        of(course),
                                                        combineLatest(
                                                            this.fieldService.get(fieldId).pipe(
                                                                map (field => field)
                                                            )
                                                        )
                                                    )
                                                }),
                                                map(([course, field]) => {
                                                    return {...course, field: field.find(f => f.id == course.fieldId)}
                                                })
                                            )
                                        ),
                                                                         
                                    )
                                }),
                                map(([session, course]) =>  {
                                    return {
                                        ...session, 
                                        course: course.find(c => c.id === session.courseId)
                                    }
                                }),
                                switchMap( session => {
                                    const sessionId = session.id;

                                    return combineLatest(
                                        of(session),
                                        combineLatest(
                                            this.TaskdatesService.getBySessionId(sessionId).pipe(
                                                switchMap(dates => {
                                                    const taskDatesIds = uniq(dates.map(dt => dt.taskId));

                                                    return combineLatest(
                                                        of(dates),
                                                        combineLatest(
                                                            taskDatesIds.map(taskDateId => this.tasksService.get(taskDateId).pipe(
                                                                map(task => task)
                                                            ))
                                                        )
                                                    )
                                                }),
                                                map(([dates, task]) => {
                                                    return dates.map(date => {
                                                        return {...date, task: task.find(t => t.id === date.taskId)}
                                                    })
                                                
                                                })
                                            )
                                        )
                                    )
                                }),
                                map(([session, dates]) => {
                                    return {
                                        ...session,
                                        dates: dates.map(date => date.find(d => d.sessionId === session.id))
                                    }
                                })
                            ))
                        )
                    )
                }),
                map(([applications, session]) => {
                    return applications.map(app => {
                        return {
                            ...app,
                            session: session.find(s => s.id === app.courseSessionId)
                        }
                    })
                })
            );
    }
}

And here is the HTML template file

    <ng-container *ngIf="joined$ | async; else loading; let joined">

    <div *ngFor="let application of joined">
        <div class="current-application">
            <nb-card>
                <nb-card-header>
                    <h4>{{application.session.course.name}}</h4>
                    <small><i>{{application.session.course.field.name}}</i></small>
                </nb-card-header>
                <nb-card-body>
                    <p><b>id: </b>{{application.id}}</p>
                    <p><b>applicationDate: </b>{{application.applicationDate}}</p>
                    <p><b>acceptedDate: </b>{{application.acceptedDate}}</p>
                    <hr>
                    <p><b>session Id: </b>{{application.session.id}}</p>
                    <p><b>session capacity: </b>{{application.session.capacity}}</p>
                    <p><b>session startDate: </b>{{application.session.startDate}}</p>
                    <p><b>session endDate: </b>{{application.session.endDate}}</p>
                    <hr>
                    <p><b>Course Id: </b>{{application.session.course.id}}</p>
                    <p><b>Course Id: </b>{{application.session.course.id}}</p>
                </nb-card-body>
            </nb-card>
        </div>
    </div>

</ng-container>

<ng-template #loading>
    <p>Loding ...</p>
</ng-template>

Edit: After debugging I found that the error occurs when Dates array is empty, so the solution consists of applying a test for the length of the dates array. The problem is when I try to make a condition I got the following error

Argument of type '(dates: SessionTaskDate[]) => void' is not assignable to parameter of type '(value: SessionTaskDate[], index: number) => ObservableInput'. Type 'void' is not assignable to type 'ObservableInput'.

triggered in the following:

switchMap(dates =>{
        if(dates.length > 0){
            dates.map(date => this.augmentDateWithTask(date))
        }
    })
2

There are 2 best solutions below

3
On BEST ANSWER

If I understand right, you have a series of applications which are notified by the Observable returned by this.applicationService.list.

Then each application has a courseSessionId which you can use to fetch course session details via this.sessionService.get method.

Then each session has a courseId which you can use to fetch course details via this.courseService.get.

Then each course has a fieldId which you can use to fetch field details via this.fieldService.get.

Now you should have an array of sessions containing also all details of the course they refer to.

Then, for each session, you need to fetch the dates via this.TaskdatesService.getBySessionId.

Dates seems to be objects containing a taskDatesId. You collect all taskDatesIds and fetch task details via this.tasksService.get.

Now that you have all dates and all tasks, for each date you create a new object with date properties and its related task.

Then you go back to the session you create a new object with all session properties and its related dates.

Now you have an object with all session details, all details of the course it refers to and all the details of the dates it has.

The last step is to create, for each application, a new object containing all properties of the application plus all properties of the "augmented" session object just created.

Quite some logic.

So, again, if this is the correct understanding, I would approach the problem from the most inner requests outwards.

The first inner request is the one that, starting from a course returns an Observable which emits an object with all course properties and the field details (let's call this object an augmentedCourse). This could be performed by a method like this

augmentCourseWithField(course) {
  return this.fieldService.get(course.fieldId).pipe(
    map (field => {
      return {...course, field}
    })
  )
}

Then I would make a step outwards and would create a method that, starting from a courseId, returns an Observable which emits an augmentedCourse, i.e. an object with all the course and the field details.

fetchAugmentedCourse(courseId) {
  return this.courseService.get(courseId).pipe(
     switchMap(course => this.augmentCourseWithField(course))
  )
}

Now let's do a further step outwards, and create a method that, starting from a session, returns an object with all properties of the session plus the properties of the augmentedCourse the session refers to. Let's call this object augmentedSession.

augmentSessionWithCourse(session) {
  return this.fetchAugmentedCourse(session.courseId).pipe(
    map(course => {
      return {...session, course}
    })
  )
}

Now one more step outwards. We want to fetch an augmentedSession starting from a courseSessionId.

fetchAugmentedSession(courseSessionId) {
  return this.sessionService.get(courseSessionId).pipe(
     switchMap(session => this.augmentSessionWithCourse(session))
  )
}

So, what did we achieve so far? We are able to create an Observable which emits an augmentedSession starting from a courseSessionId.

At the outermost level though we have a list of applications, each of which contains a courseSessionId. Different applications can share the same course and so have the same courseSessionId. Therefore it makes sense to fetch all applications, create a list of unique courseSessionIds, use such list to fetch all the courses and then assign to each application its course. In this way we avoid querying the back end more than once for the same course.

This can be acheived like this

fetchAugmentedApplications(user) {
  return this.applicationService.list(1, 10, undefined, user.id).pipe(
    switchMap(applications => {
      const courseSessionIds = uniq(applications.map(application => application.courseSessionId));
      // create an array of Observables, each one will emit an augmentedSession
      const augSessObs = courseSessionIds.map(sId => fetchAugmentedSession(sId))
      // we can use combineLatest instead of forkJoin, but I prefer forkJoin
      return forkJoin(augSessObs).pipe(
        map(augmentedSessions => {
          return applications.map(app => {
            const appSession = augmentedSessions.find(s => s.id === app.courseSessionId);
            return {...app, session: appSession}
          });
        })
      )
    })
  )
}

With a similar style you should be able to add also dates details to your sessions. Also in this case we start with innermost operation, which is to retrieve an array of tasks given an array of taskIds via his.tasksService.get.

The code would look like this

fetchTasks(taskIds) {
   taskObs = taskIds.map(tId => this.tasksService.get(tId));
   return forkJoin(taskObs)
}

Moving one step outward, we can augment each date of an array of dates with tasks that belong to that date like this

augmentDates(dates) {
  if (dates.length === 0) {
    return of([]);
  }
  const taskDatesIds = uniq(dates.map(dt => dt.taskId));
  return fetchTasks(taskDatesIds).pipe(
    map(tasks => {
      return dates.map(date => {
        return {...date, task: task.find(t => t.id === date.taskId)}
      })
    })
  )
}

Now we can augment a session with its dates like this

augmentSessionWithDates(session) {
  return this.TaskdatesService.getBySessionId(session.id).pipe(
    switchMap(dates => augmentDates(dates).pipe( 
      map(augmentedDates => {
        return {...session, dates: augmentedDates};
      })
    ))
  )
}

We can now complete the fetchAugmentedSession method, adding also the part that augment the session with dates info like this

fetchAugmentedSession(courseSessionId) {
  return this.sessionService.get(courseSessionId).pipe(
     switchMap(session => this.augmentSessionWithCourse(session)),
     switchMap(session => this.augmentSessionWithDates(session)),
  )
}

In this way you have split your logic in smaller chunks, which are easier to test and, hopefully, easier to read.

I did not have any playground to test the code, so it is very well possible that there are typos in it. I hope though that the logic is clear enough.

4
On

That's a lot of async nesting, perhaps there is another way to go about this?

If you are trying to take multiple observables and have a single subscription (say, be able to react to a number of events in a similar manner), then use rxjs merge.

If you are wanting to do a lot of individual subscriptions, like subscribe to users, foo, and bar and then use all of their values for some logic, use rxjs forkJoin.

If you state a clear objective it would be easier to offer guidance, but I know that level of rxjs operator nesting is not easily maintained.