2

I have a component where a user chooses a report type to generate.

The component.ts function is like so:

generateReport(reportType, projectReport) {
  this.showSpinner = true;
  if(reportType === 'project-weekly-report')
    this.router.navigate(['/weeklyReport', projectReport]).catch<any>(this.handleError.bind(this));
  else this.router.navigate(['/monthlyReport']).catch<any>(this.handleError.bind(this));
}

This route it forwards to has a component that uses a resolver to handle the loading of the report. The ngOnInit() of the component looks like this:

ngOnInit(): void {
   this.report = this.route.snapshot.data['projectWeeklyReport'].result;
}

The resolver looks like this:

@Injectable()
export class ProjectWeeklyReportResolve implements Resolve<any> {

  constructor(private reportService: ReportService, private http: HttpClient) {}

  resolve(route: ActivatedRouteSnapshot) {
    return this.reportService.getProjectWeeklyReport(route.paramMap.get('projectName'));
  }
}

This resolver calls the following function in the service:

getProjectWeeklyReport(projectName) {
  var workerUrl = this.reportUrl + '/weeklyreport/projectname/' + projectName + '/job/';
  return this.http
    .get<any>(this.reportUrl + '/weeklyreport/projectname/' + projectName, this.getAuthOptions(true))
    .pipe(
      switchMap(workerId => this.pollFor(workerId, isWorkerCompleted, isWorkerFailed, 2000, workerUrl)),
      catchError(this.handleError)
    );
}

This method hits the server to tell it to start generating the report. As the report takes some time to generate, this the job has been hived off to a worker job. We then poll this worker job to check if the report has completed generating the report.

The polling function looks like this:

type CustomPollOperator = (data: any, condComp: (d) => boolean, condFail: (d) => boolean, ms: number, url: string) => Observable<any>

const isWorkerCompleted = w => w.result;
const isWorkerFailed = w => w.failedReason;

//Convenience method for polling operation
pollFor: CustomPollOperator = (data, condComp, condFail, ms, url) => {
  let shouldPoll = true;

  return interval(ms)
    .pipe(
      tap(() => console.warn('polling', shouldPoll)),
      takeWhile(() => shouldPoll),
      switchMap(() => this.http.get<any>(url + data.id, this.getAuthOptions(true))),
      tap(res => {
        if(condComp(res)) {
          shouldPoll = false;
        }
        else if(condFail(res)) {
          shouldPoll = false;
          throw new Error('Polling worker job failed');              
        }
      }),
      catchError(this.handleError)
    )
}

Before I updated from angular 9 to 14 the resolver would receive a finished report when the switchMap and the CustomPollOperator had finished it's job.

However, after updating the Angular version, the resolver does not wait for the polling to finish, and loads the page immediately after the first request that kicks of the job.

Any ideas what is causing this problem? I use the same code elsewhere when there is not a resolver involved and it functions as it did before.

Many thanks in advance!

Tom O'Brien
  • 1,741
  • 9
  • 45
  • 73
  • When you say from angular 9 to 14 do you mean via 10-13? It's not recommended to jump major versions. See https://update.angular.io/ – Andrew Allen Mar 24 '23 at 15:58
  • 1
    @AndrewAllen - out of my control - decision was made to jump from 9-14. This is the only remaining issue :) – Tom O'Brien Mar 24 '23 at 16:09
  • 1
    I have a guess that your resolver gets a value. Your observable appears to be returning poll results. This way resolvers thinks its data appeared. To fix it you need to filter out polling process from the resulting stream. You can confirm my guess by piping a `tap(console.log, console.error, () => console.info('finished'))` to the observable in the resolver – Sergey Mar 27 '23 at 06:09

1 Answers1

1

pollFor will emit the first result of this line:

switchMap(() => this.http.get<any>(url + data.id, this.getAuthOptions(true))),

which will cause the route to resolve.

If you want to stop that from happening you need to apply a filter. I would just exchange the tap for a filter, and return true when the polling is complete, and false otherwise. This will only emit the result once polling is complete.

return interval(ms).pipe(
    tap(() => console.warn('polling', shouldPoll)),
    takeWhile(() => shouldPoll),
    switchMap(() =>
      this.http.get<any>(url + data.id, this.getAuthOptions(true))
    ),
    filter((res) => {
      if (condComp(res)) {
        shouldPoll = false;
        return true;
      } else if (condFail(res)) {
        shouldPoll = false;
        throw new Error('Polling worker job failed');
      }
      return false;
    }),
    catchError(this.handleError)
  );

I'm not really sure why this would act differently in Angular 9, although I've never used a resolver in that version. Are you sure it used to be polling multiple times before resolving? Keep in mind interval does not emit immediately. The interval timing is also the initial delay.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • Hi Chris - thanks for that. When I change the tap to filter, it now gives the following error about the type of "res": error: No overload matches this call. Overload 1 of 2, '(predicate: (value: any, index: number) => value is any, thisArg?: any): OperatorFunction', gave the following error. Argument of type '(res: any) => void' is not assignable to parameter of type '(value: any, index: number) => value is any'. Signature '(res: any): void' must be a type predicate. I'm not sure why it is complaining as i have specified "any" in the http.get() – Tom O'Brien Mar 27 '23 at 18:55
  • It's telling you the return type is `void`, meaning you're not returning a boolean within the filter predicate? Double check `return false` is outside both conditions. ie. nothing should be emitted when the poll has neither completed nor failed. – Chris Hamilton Mar 27 '23 at 19:05
  • Hmmmm. The return type of the https request is definitely not void, as i use this code elsewhere. This exact same code with the tap works elsewhere, where a resolver is not involved. I'm new to this stuff, and confused why the filter would think it would be void, but the tap not? – Tom O'Brien Mar 27 '23 at 19:13
  • The return type of the filter predicate should be a boolean. The predicate is the function you pass in as a parameter to `filter`. ie. `filter((res) => { // This function should return a boolean but it is not returning anything })` – Chris Hamilton Mar 27 '23 at 19:14
  • 1
    @TomO'Brien maybe you missed the two lines I added within that function? – Chris Hamilton Mar 27 '23 at 19:24
  • That has done the trick! My bad - i didn't see those lines... doh! Thanks Chris. – Tom O'Brien Mar 27 '23 at 19:32