Leveraging the expand function for pagination through recursive invocations

I am currently working on retrieving data from a third party API that necessitates manual management of paging by keeping track of the number of records retrieved versus the total number of records available.

In attempting to handle this, I experimented with utilizing the expand and reduce operators in RxJs. Although, I have encountered an issue of infinite looping which I haven't faced before when dealing with APIs that offer a 'nextPage' link.

It seems that the root cause stems from the nextPosition variable not being appropriately updated when the output of expand is executed repeatedly within the expansion loop. At this point, I am unsure whether this method can resolve the problem at hand.

My primary question revolves around the feasibility of incorporating expand and reduce alongside a recursive function, and if so, what modifications should be made to rectify the following code snippet?

private async retrievePagedRecords<T>(companyURL: string, query: string, startPosition: number, totalCount: number, transformFn: (qbData: any) => T[]): Promise<T[]> {

        const headers = this.getHttpConfig();
        let pageQuery = `${query} STARTPOSITION ${startPosition} MAXRESULTS ${this.pagingSize}`;
        const nextPosition = startPosition + this.pagingSize;

        const records = await lastValueFrom(this.http.get(`${companyURL}/query?query=${pageQuery}`, headers)
            .pipe(
                map(x => x.data as any),
                map(x => {
                    //Trivial transformation to property names etc.
                    return transformFn(x);
                }),
                expand(x => nextPosition > totalCount ? [] : this.retrievePagedRecords<T>(companyURL, query, nextPosition, totalCount, transformFn)),
                reduce((acc: T[], x: T[]) => acc.concat(x ?? []), []),
                catchError(error => {
                    return of([]);
                })
            ));
        return records;
    }

Answer №1

When using the EXPAND operator, it is important to check for a falsy condition and return EMPTY in order to proceed.

 import { EMPTY } from 'rxjs';

 expand(x => nextPosition > totalCount ? EMPTY : this.fetchPagedData<T>(url, query, nextPosition, totalCount, transformFn)),

To learn more about the EXPAND operator, take a look at my post on recursive HTTP calls with RxJS here.

Answer №2

Your function seems to be stuck in a recursive loop. Consider utilizing the expand method to iteratively call the API instead.

private pagingSize: number;

private async fetchPagedData<T>(
  companyURL: string, 
  query: string,
  totalCount: number, 
  transformFn: (qbData: any) => T[]
): Promise<T[]> {

  const headers = this.getHttpConfig();

  let queryString = startPos => `${companyURL}/query?query=${query} STARTPOSITION ${startPos} MAXRESULTS ${this.pagingSize}`;

  let queryStream = startPos => this.http.get(queryString(startPos), headers).pipe(
    map(payload => ({
      payload,
      currentPos: startPos
    })),
  );

  const queryResult$ = queryStream(0).pipe(
    
    expand(({currentPos}) => 
      currentPos > totalCount ? 
      EMPTY : 
      queryStream(currentPos + this.pagingSize)
    ),

    // Simplifying data structure names, etc.
    map(({payload}) => transformFn(payload.data)),
    // Merge results
    reduce((acc: T[], x: T[]) => acc.concat(x ?? []), []),
    catchError(error => {
        return of([]);
    })
  );

  return await lastValueFrom(queryResult$);
}

Answer №3

It seems unnecessary to reference the same function within expand. "Expand" already involves recursion.

You can save retrieved data in "of()" and provide the correct "pageID" there.

In my situation, if "pageId" is null => I must end the recursion and return EMPTY

You can refer to my example

public loadAllAlarms(params: AlarmRequest, isActiveAPI: boolean,  maxCount = null): Observable<AlarmDefinition[]> {
    console.log(params);
    return of({page: undefined, alarms: [], counts: 0}).pipe(
        expand(data => data.page === null
            ? EMPTY
            : this.AMapi.loadAlarms({...params, ...(!data.page ? {} : {pageState: data.page})}, isActiveAPI)
                .pipe(
                    map(response => {
                    const counts = data.counts + response.results.length;
                    return ({
                        page: (maxCount && (counts >= maxCount)) ? null : response.pageState,
                        alarms: response.results,
                        counts
                    })
                }))
        ),
        reduce((acc, items) => ([...acc, ...items.alarms]), []),
        map(data => maxCount ? data.slice(0, maxCount) : data)
    )
}

some unit tests

it('should call API until pageState === null is encountered', () => {
  const countExistedPages = 3;
  let countCalls = 0;
  const spy = spyOn(AMapi, 'loadAlarms').and.callFake(() => {
    countCalls += 1;
    return of({pageState: countCalls === countExistedPages ? null : 'id', results: new Array(10)} as AlarmPagedResponse);
  });

  service.loadAllAlarms({} as any, true).subscribe(data => expect(data.length).toEqual(30));
  expect(spy).toHaveBeenCalledTimes(3);
});

it('should call API until maxCount is reached', () => {
  const countExistedPages = 3;
  let countCalls = 0;
  const spy = spyOn(AMapi, 'loadAlarms').and.callFake(() => {
    countCalls += 1;
    return of({pageState: countCalls === countExistedPages ? null : 'id', results: new Array(10)} as AlarmPagedResponse);
  });

  service.loadAllAlarms({} as any, true, 15).subscribe(data => expect(data.length).toEqual(15));
  expect(spy).toHaveBeenCalledTimes(2);
});

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

VPS mysteriously terminates TypeScript compilation process without any apparent error

I've encountered an issue while trying to compile my TypeScript /src folder into the /dist folder. The process works smoothly on my local machine, but when I clone the repo onto my VPS (Ubuntu Server 22.04), install npm, and run the compile script, it ...

Error: Failed to retrieve the name property of an undefined value within the Array.forEach method

Upon pressing the button to display the task pane, I encountered an error message in the console window that reads: "Uncaught (in promise) TypeError: Cannot read property 'name' of undefined". This error persists and I am unable to resolve or com ...

Can ng-content be utilized within the app-root component?

I have successfully developed an Angular application, and now I am looking to integrate it with a content management system that generates static pages. In order to achieve this, I need to utilize content projection from the main index.html file. The desi ...

Transform leaflet marker plugin into Typescript format

I recently discovered a leaflet extension that conceals map markers if they fall outside the boundaries of the current view map. import L from 'leaflet'; L.Marker.MyMarker= L.Marker.extend({}).addInitHook(function (this: ILazyMarker) { this ...

Checking nested arrays recursively in Typescript

I'm facing difficulty in traversing through a nested array which may contain arrays of itself, representing a dynamic menu structure. Below is how the objects are structured: This is the Interface IMenuNode: Interface IMenuNode: export interface IM ...

What is the best method to display a service property within a controller?

If we consider the scenario where I have a controller named ctrlA with a dependency called serviceB, which in turn has a property known as propertyC. My development environment involves Angular and Typescript. When interacting with the user interface, the ...

The error occurred in Commands.ts for Cypress, stating that the argument '"login"' cannot be assigned to the parameter of type 'keyof Chainable<any>))`

Attempting to simplify repetitive actions by utilizing commands.ts, such as requesting email and password. However, upon trying to implement this, I encounter an error for the login (Argument of type '"login"' is not assignable to parameter of t ...

Creating Production Files for Web using RxJs and TypeScript

I am interested in developing a JavaScript Library using RxJs (5.0.0-Beta.6) and TypeScript (1.8.10). My TypeScript file is successfully compiling. Below are the files I have: MyLib.ts: /// <reference path="../../typings/globals/es6-shim/index.d.ts" ...

What is the best way to bring in an array from a local file for utilization in a Vue 3 TypeScript project?

Encountering a TS7016 error 'Could not find a declaration file for module '../composables/httpResponses'. '/Users/username/project/src/composables/httpResponses.js' implicitly has an 'any' type.' while attempting to ...

An easy guide to using validators to update the border color of form control names in Angular

I'm working on a form control and attempting to change the color when the field is invalid. I've experimented with various methods, but haven't had success so far. Here's what I've tried: <input formControlName="pe ...

For editing values that have been dynamically inserted

In my JSON data, there is a variable named address that contains multiple objects (i.e., multiple addresses). I am displaying these multiple addresses as shown in the following image: When clicking on a specific address (e.g., addressType: Business), t ...

What is the process for invoking a service from a component?

I'm currently facing an issue with my service that is responsible for making HTTP requests and returning responses. The problem arises when I try to display parts of the response on the screen within my component, as nothing seems to be showing up des ...

Obtaining the current row index in React MUI Data Grid using React-Context

Scenario In my application, I have implemented an MUI Data Grid with custom components in each row: RowSlider, RowDate, and RowLock using the MUI Components Slider, Date Picker, and Button respectively. View the Data Grid Visualization The Slider and Da ...

Having trouble with NPM install freezing during package installations?

Hello, I am currently facing an issue with my project. It works perfectly fine, but the problem arises when I try to move the project files (except node_modules) to another folder or GitHub. The reinstallation of packages via npm always gets stuck at: ex ...

Searching for a string within a JSON object in Angular: step-by-step guide

JSON Data Example { "rootData": { "test1": { "testData0": "Previous information", "testData1": "Earlier Information" }, "test2": { "testData0": ...

How to successfully utilize TypeScript ES6 modules and RequireJS for registering Angular elements

I am in the process of updating a current Angular application that uses AMD with TypeScript 1.5 ES6 module syntax. Our modules are currently stored in separate files, and the main "app" module is defined like this... define('app', ['angular ...

Deriving a universal parameter from a function provided as an argument

My function can take in different adapters along with their optional options. // Query adapter type 1 type O1 = { opt: 1 } const adapter1 = (key: string, options?: O1) => 1 // Query adapter type 2 type O2 = { opt: 2 } const adapter2 = (key: string, opti ...

Adding fresh 'observers' to a list of 'observers' while the application is running and monitoring updates by utilizing combineLatest in Angular

In my current scenario, I am facing a challenge of managing a list of observables that are constantly being updated by adding new observables to it during the application's runtime. Within this list, I am using combineLatest to perform API calls wit ...

`Why isn't GetServerSideProps being triggered for a nested page in Next.js when using Typescript?

I have been working on a page located at /article/[id] where I am trying to fetch an article based on the id using getServerSideProps. However, it seems that getServerSideProps is not being called at all as none of my console logs are appearing. Upon navi ...

How should one begin a new NativeScript-Vue project while implementing Typescript support in the most effective manner?

Is it possible to incorporate Typescript into Vue instance methods? I found guidance on the blog page of nativescript-vue.org. Whenever I initiate a new nativescript-vue project using vue init nativescript-vue/vue-cli-template <project-name>, some w ...