网站搜索

javascript - 如何限制返回输出的 TypeScript 函数的调用频率


我正在使用 TypeScript 编写一个 node.js 应用程序。我的应用程序将有多个服务相互通信。其中几个服务需要调用外部 API。这个 API 对每秒可执行的调用次数有一个限制。因为这个原因,我想创建一个服务来封装对外部 API 的调用(我们称之为 ApiService)。其他服务将调用这个服务,它会将它们的请求收集到队列中并按顺序执行——每秒 N 个请求(为了简单起见,假设每秒一个)。当服务 A 调用 ApiService 的某个方法时——它期望收到一个输出(接收 Promise 也是可以的)。

现在我的问题是 - 如何在 ApiService 中排队这些 API 调用,以便每隔 1 秒执行队列中的下一个调用,并将该 API 调用的输出返回给 ApiService 的调用者?

这是一个示例服务:

export class ServiceA {
   apiService: ApiService;

   public constructor(_apiService: ApiService) {
      apiService = _apiService;
   }

   public async DoWork() {
      // Do some stuff
      const output: number = await apiService.RetrieveA(param1, param2);
      // Do something with the output
   }
}

ApiService:

export class ApiService {
   queue: (() => Promise<any>)[] = [];

   public async RetrieveA(param1, param2): Promise<number> {
      const func = async () => {
         return this.CallApi(param1, param2);
      };

      this.queue.push(func);
      return func();
   }

   public async RunQueue() {
      while(true) {
         const func = this.queue.shift();
         if (!func) { continue; }
         // Call the function after 1 second
         await setTimeout(() => { func(); }, 1000);
      }
   }

   private async CallApi(param1, param2): Promise<number> {
      // Call the external API, process its output and return
   }
}

协调整个过程的主方法:

var CronJob = require('cron').CronJob;

const apiService = new ApiService();
const service = new ServiceA(apiService);

new CronJob('* * * * * *', function() {
   service.DoWork();
}, null, true);

apiService.RunQueue();

我面临的问题是,当 RetrieveA 方法返回 func() 时——函数会立即执行。我需要返回一个 Promise,但实际的函数执行需要发生在 RunQueue() 方法中。有没有办法实现这一点?我可以返回一个 Promise 而不立即执行函数,并且在等待这个 Promise 时——在 RunQueue 方法中调用函数时收到输出吗?

还是有其他方法可以解决我处理返回输出的 API 调用限制的问题?

我刚接触 Node.js/Typescript/JavaScript,所以非常感谢任何帮助 :)

答案#1

如果你想将对 RetreiveA 的调用限制为每秒 2 次,所有这些可以变得简单得多:

//lib is here: https://github.com/amsterdamharu/lib/blob/master/src/index.js
import * as lib from '../../src/index'
const twoPerSecond = lib.throttlePeriod(2,1000);
export class ApiService {
  public RetrieveA(param1, param2): Promise<number> {
    //removed the resolver part, according to the typescript signature
    //  it should return a promise of number but resolver actually takes
    //  that number and returns void (undefined?)
    return twoPerSecond(this.CallApi.bind(this))([param1, param2]);
  }
  //change the signature of this function to take one parameter
  //  but deconsruct the array to param1 and param2
  private async CallApi([param1, param2]): Promise<number> {
    // Call the external API, process its output and return
  }
}

你的方法只有在该类只有一个实例时才有效。如果你创建多个实例并在这些实例上调用 RetrieveA,你将不再限制对 callApi 的请求。

答案#2

我确实设法找到了一种可行的解决方案。我对 JavaScript 中的 Promise 和异步概念不是很熟悉,所以这可能不是最好的解决办法,但它对于我的特定情况可以发挥作用。以下是代码,供其他想实现类似功能的人参考:

示例 ServiceA 保持与上述相同:

export class ServiceA {
   apiService: ApiService;

   public constructor(_apiService: ApiService) {
      apiService = _apiService;
   }

   public async DoWork() {
      // Do some stuff
      const output: number = await apiService.RetrieveA(param1, param2);
      // Do something with the output
   }
}

这是修改后的 ApiService,它返回输出的 Promise 并限制实际函数的执行频率:

export class ApiService {
   // We keep the functions that need to be executed in this queue
   // and process them sequentially
   queue: (() => void)[] = [];

   public async RetrieveA(param1, param2): Promise<number> {
      // This resolver serves two purposes - it will be called when the
      // function is executed (to set the output), but will also be part
      // of the Promise that will be returned to the caller (so that the
      // caller can await for the result).
      let resolver: (value: number) => void;

      // This function will be executed by the RunQueue method when its
      // turn has come. It makes a call to the external API and when that
      // call succeeds - the resolver is called to return the result through
      // the Promise.
      const func = async () => {
         return this.CallApi(param1, param2).then(resolver);
      };

      this.queue.push(func);

      // This is the promise that we return to the caller, so that he
      // can await for the result. 
      const promise = new Promise<number>((resolve, reject) => {
         resolver = resolve;
      });

      return promise;
   }

   public async Run() {
      this.RunQueue(this.queue);
   }

   private async RunQueue(funcQueue: (() => void)[]) {
      // Get the first element of the queue
      const func = funcQueue.shift();

      // If the queue is empty - this method will continue to run
      // until a new element is added to the queue
      if (func) {
         await func();
      }

      // Recursively call the function again after 1 second
      // This will process the next element in the queue
      setTimeout(() => {
         this.RunQueue(funcQueue);
      }, 1000);
   }

   private async CallApi(param1, param2): Promise<number> {
      // Call the external API, process its output and return
   }
}

我希望代码中的注释能清楚地说明我正在尝试实现的内容(以及方式)。