Turning paginated requests into an Observable stream with RxJs

后端 未结 4 818
渐次进展
渐次进展 2020-12-30 10:28

I have a service which returns data in pages. The response to one page contains details on how to query for the next page.

My approach is to return the response data

相关标签:
4条回答
  • 2020-12-30 10:32

    Here is a more concise & IMHO clean answer without any recursion. It's using ogen(~46 loc) to transform any generator into an observable.

    It has a custom built next function that will emit data anytime your function yield something.

    nb: The original article is worth reading

    function getPagedItems({offset=0, limit=4}) {
        paginatedQueryGenerator = function*(someParams offset, limit) {
            let hasMore = true
            while(hasMore) {
                const results = yield YOUR_PROMISE_BASED_REQUEST(someParams, limit, offset)
                hasMore = results && results.nextpage !== null 
                offset += limit
            }
        }
    
        return ogen(paginatedQueryGenerator)(someParams, offset, limit)
    } 
    
    0 讨论(0)
  • 2020-12-30 10:37

    EDIT Ah! I see the problem you're facing. A bit of tail call optimization should fix you up:

    function mockGetPageAjaxCall(index) {
      // return dummy data for testcase
      return Promise.resolve({nextpage:index+1, data:[1,2,3]});
    }
    
    function getPageFromServer(index) {
      return Observable.create(function(obs) {
        mockGetPageAjaxCall(index).then(function(page) {
          obs.onNext(page);
        }).catch(function(err) {
          obs.onError(err)
        }).finally(function() {
          obs.onCompleted();
        });
      });
    }
    
    function getPagedItems(index) {
        return Observable.create(function(obs) {
            // create a delegate to do the work
            var disposable = new SerialDisposable();
            var recur = function(index) {
                disposable.setDisposable(getPageFromServer(index).retry().subscribe(function(page) {
                    obs.onNext(page.items);
                    if(page.nextpage === null) {
                      obs.onCompleted();   
                    }
    
                    // call the delegate recursively
                    recur(page.nextpage);
                }));
            };
    
            // call the delegate to start it
            recur(0);
    
            return disposable;
        });
    }
    
    getPagedItems(0).subscribe(
      function(item) {
        console.log(new Date(), item);
      },
      function(error) {
        console.log(error);
      }
    )
    
    0 讨论(0)
  • 2020-12-30 10:52

    Looking at the OP code, it is really the right method. Just need to make your mock service asynchronous to simulate real conditions. Here is a solution that avoids the stack exhaustion (I also made getPageFromServer actually return a cold observable instead of requiring the caller to wrap it).

    Note that if you really expect your service requests to complete synchronously in the real application and thus you need to ensure your code does not exhaust the stack when this happens, just have getPagedItems() invoke the currentThread scheduler. The currentThread scheduler schedules tasks using a trampoline to prevent recursive calls (and stack exhaustion). See the commented out line at the end of getPagedItems

    function getPageFromServer(index) {
        // return dummy data asynchronously for testcase
        // use timeout scheduler to force the result to be asynchronous like
        // it would be for a real service request
        return Rx.Observable.return({nextpage: index + 1, data: [1,2,3]}, Rx.Scheduler.timeout);
    
        // for real request, if you are using jQuery, just use rxjs-jquery and return:
        //return Rx.Observable.defer(function () { return $.ajaxAsObservable(...); });
    }
    
    function getPagedItems(index) {
        var result = getPageFromServer(index)
            .retry(3) // retry the call if it fails
            .flatMap(function (response) {
                var result = Rx.Observable.fromArray(response.data);
                if (response.nextpage !== null) {
                    result = result.concat(getPagedItems(response.nextpage));
                }
                return result;
            });
    
        // If you think you will really satisfy requests synchronously, then instead
        // of using the Rx.Scheduler.timeout in getPageFromServer(), you can
        // use the currentThreadScheduler here to prevent the stack exhaustion...
    
        // return result.observeOn(Rx.Scheduler.currentThread) 
        return result;
    }
    
    0 讨论(0)
  • 2020-12-30 10:56

    Another one solution is to use retryWhen

    getAllData() {
        let page = 0;
        let result = [];
    
        const getOnePage = () => {
            return of(0).pipe(mergeMap(() => getPaginatedData(page++)));
        };
    
        return getOnePage()
            .pipe(
                map(items => {
                    result = result.concat(items);
                    if (templates.length === PAGE_SIZE) {
                        throw 'get next page';
                    }
                }),
                retryWhen(e => e.pipe(
                    takeWhile(er => er === 'get next page'))
                ),
                map(e => result)
            )
            .subscribe(items => {
                console.log('here is all data', items);
            });
    
    }
    
    0 讨论(0)
提交回复
热议问题