'How to refactor recursive method to use RxJs Observables for pagination

I'm new to RxJs

I have a method that paginates through all pages available on a url and stores the json response in an array, using a recursive approach.

Is there a simple way to achieve the same outcome using RxJs observables without recursion?

    ...

    export interface Contestant {
      rank: number;
      finish_time: number;
      score: number;
    }

    export interface Contest {
      url: URL;
      contestNumber: number;
      lastPage: number;
      /**
       * Number of contestants.
       * Gotten as lastPage x 25.
       */
      totalContestants: number;
    }

    export interface Response {
      total_rank: Contestant[];
    }
    ...

  /**
   * Scrape all useful entries from a contest
   * and return an array of entries
   **/
  async scrapeContestData(contest: Contest): Promise<Contestant[]> {
    /**
     * This recursive approach assumes 500bytes per stack call
     * and a lastPage typically <= 1000
     * for a rough estimation of 500kb of stack space needed
     *
     * The default node stack size is 984kb so for a typical
     * contest this should be fine. If not, needs refactoring.
     */
    if (contest.lastPage === 0) return [];
    contest.url.searchParams.set('pagination', contest.lastPage.toString());

    const res = await firstValueFrom(
      this.httpService.get<Response>(contest.url.toString()),
    );

    contest.lastPage--;

    return res.data.total_rank.concat(await this.scrapeContestData(contest));
  }


Solution 1:[1]

I might try something like this:

Most recursion in RxJS can be nicely handled by expand. In this case, however, I've removed the recursion in favor of a simple range, since your algorithm isn't really making choices between recursive calls.

I haven't tested this, but with a bit of tinkering it should work.

scrapeContestData(contest: Contest): Observable<Contestant[]> {

  return range(0, contest.lastPage - 1).pipe(
    map(v => contest.lastPage - v),
    concatMap(page => {
      const url = contest.url;
      url.searchParams.set("pagination", page.toString());
      return this.httpService.get<Response>(url.toString()).pipe(take(1))
    ),
    toArray(),
    map((datum: Contestant[][]) => datum.flat())
  );

}

A quick aside:

Arrays and Observables are abstractly very similar things. One is a list across space (in memory) and the other is a list across time (in CPU cycles).

All that to say, the abstract collection-type functions that exist on arrays mostly all exist for Observables as well.

We could re-write this without Array#flat like this:

scrapeContestData(contest: Contest): Observable<Contestant[]> {

  return range(0, contest.lastPage - 1).pipe(
    map(v => contest.lastPage - v),
    concatMap(page => {
      const url = contest.url;
      url.searchParams.set("pagination", page.toString());
      return this.httpService.get<Response>(url.toString()).pipe(take(1))
    ),
    concatMap(v => v),
    toArray()
  );

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1