How to limit API calls per second with angular2

2019-05-13 19:43发布

问题:

I have an API limit of 10 calls per second (however thousands per day), however, when I run this function (Called each Style ID of object, > 10 per second):

  getStyleByID(styleID: number): void {
    this._EdmundsAPIService.getStyleByID(styleID).subscribe(
      style => {this.style.push(style); },
      error =>  this.errorMessage = <any>error);
  }

from this function (only 1 call, used onInit):

  getStylesWithoutYear(): void {
    this._EdmundsAPIService.getStylesWithoutYear(this.makeNiceName, this.modelNiceName, this.modelCategory)
      .subscribe(
        styles => { this.styles = styles;
                      this.styles.years.forEach(year =>
                        year.styles.forEach(style =>
                          this.getStyleByID(style.id)));

        console.log(this.styles); },
        error =>  this.errorMessage = <any>error);
  }

It makes > 10 calls a second. How can I throttle or slow down these calls in order to prevent from getting a 403 error?

回答1:

I have a pretty neat solution where you combine two observables with the .zip() operator:

  1. An observable emitting the requests.
  2. Another observable emitting a value every .1 second.

You end up with one observable emitting requests every .1 second (= 10 requests per second).

Here's the code (JSBin):

// Stream of style ids you need to request (this will be throttled).
const styleIdsObs = new Rx.Subject<number>();

// Getting a style means pushing a new styleId to the stream of style ids.
const getStyleByID = (id) => styleIdsObs.next(id);

// This second observable will act as the "throttler".
// It emits one value every .1 second, so 10 values per second.
const intervalObs = Rx.Observable.interval(100);

Rx.Observable
  // Combine the 2 observables. The obs now emits a styleId every .1s. 
  .zip(styleIdsObs, intervalObs, (styleId, i) => styleId)
  // Get the style, i.e. run the request.
  .mergeMap(styleId => this._EdmundsAPIService.getStyleByID(styleId))
  // Use the style.
  .subscribe(style => {
    console.log(style);
    this.style.push(style);
  });

// Launch of bunch of requests at once, they'll be throttled automatically.
for (let i=0; i<20; i++) {
  getStyleByID(i);
}

Hopefully you'll be able to translate my code to your own use case. Let me know if you have any questions.

UPDATE: Thanks to Adam, there's also a JSBin showing how to throttle the requests if they don't come in consistently (see convo in the comments). It uses the concatMap() operator instead of the zip() operator.



回答2:

You could use a timed Observable that triggers every n milliseconds. I didn't adapt your code but this one shows how it would work:

someMethod() {
  // flatten your styles into an array:
  let stylesArray = ["style1", "style2", "style3"];

  // create a scheduled Observable that triggers each second
  let source = Observable.timer(1000,1000);
  // use a counter to track when all styles are processed
  let counter = 0;

  let  subscription = source.subscribe( x => {
    if (counter < stylesArray.length) {
        // call your API here
        counter++;
    } else {
        subscription.complete();
    }
  });
}

Find here a plunk that shows it in action



回答3:

While I didn't test this code, I would do try something along these lines.

Basically I create a variable that keeps track of when the next request is allowed to be made. If that time has not passed, and a new request comes in, it will use setTimeout to allow that function to run at the appropriate time interval. If the delayUntil value is in the past, then the request can run immediately, and also push back the timer by 100 ms from the current time.

delayUntil = Date.now();

getStylesWithoutYear(): void {
  this.delayRequest(() => {
    this._EdmundsAPIService.getStylesWithoutYear(this.makeNiceName, this.modelNiceName, this.modelCategory)
      .subscribe(
        styles => { this.styles = styles;
                    this.styles.years.forEach(year =>
                      year.styles.forEach(style =>
                        this.getStyleByID(style.id)));

        console.log(this.styles); },
        error =>  this.errorMessage = <any>error);
  };        
}

delayRequest(delayedFunction) {
  if (this.delayUntil > Date.now()) {
    setTimeout(delayedFunction, this.delayUntil - Date.now());
    this.delayUntil += 100;
  } else {
    delayedFunction();
    this.delayUntil = Date.now() + 100;
  }
}