Модуль очереди:
/**
* Hold Your Horses!
* Promise-based dispatcher that respects frequency limits.
* It queues requests so that no more than N are processed within 1 second.
* Those can go in parallel.
*
* Instantiate the new HH with options, specifying time limit.
* Method .add(function) adds a new job to the queue.
* Argument function should return Promise object that starts to process only after the function is called.
* @return Object Promise.
*/
function HorsesHolder(options) {
options = options || {};
this.rps = options.rps || 3; // requests per second
this.parallel = options.parallel || this.rps; // max parallel running jobs
this.times = []; // -1: slot is busy, 0: slot is free, positive timestamp - time slot's job has finished
for (let i=0; i<this.rps; i++) this.times.push(0); // [0, 0, 0] initially
this.queue = [];
this.inprogress = [];
this.debug = !!options.debug;
this.debug && console.log("%s ms: [HH] initialized", this.ts());
}
HorsesHolder.prototype.add = function(promiseMaker) {
var self = this;
return new Promise(function(resolve, reject) {
self.queue.push({
resolve: resolve,
reject: reject,
promiseMaker: promiseMaker,
});
self._ping();
});
};
// Decide: work or wait
HorsesHolder.prototype._ping = function() {
if (this.queue.length === 0) {
this.debug && console.log("%s ms: [ping] queue is empty", this.ts());
return;
}
const best = this._bestTime();
if (best === -1) {
this.debug && console.log("%s ms: [ping] cannot go: %s", this.ts(), JSON.stringify(this.times));
return;
}
const index = this.times.indexOf(best);
this.debug && console.info("%s ms: [ping] exec now at index %d", this.ts(), index);
this._execute(index);
}
/**
* Out of current times[] finds the best to occupy, if possible;
* otherwise -1
*/
HorsesHolder.prototype._bestTime = function() {
let best = -1;
for (let i=0; i<this.rps; i++) {
const time = this.times[i];
if (time === 0) return 0; // can go now - nothing better!
if (time < 0) continue; // previous not finished yet
if (this.ts() < time + 1000) continue; // not yet
if (best === -1) best = time;
else best = Math.min(best, time);
}
return best;
}
HorsesHolder.prototype.ts = function() {
return (new Date()).getTime();
}
HorsesHolder.prototype._execute = function(index) {
this.times[index] = -1; // mark busy
const job = this.queue.shift();
this.inprogress.push(job);
const self = this;
job.promiseMaker()
.then(function(r) {
self.debug && console.info("%s ms: [HH] Job done at index %d", self.ts(), index);
job.resolve(r);
})
.catch(function(err){
self.debug && console.error("%s ms: [HH err] Error at index %d: %s", self.ts(), index, err.toString());
job.reject(err);
})
.finally(function(){
self.inprogress.splice( self.inprogress.indexOf(job), 1);
self.times[index] = self.ts();
setTimeout(() => self._ping(), 1000);
});
}
export default HorsesHolder;
Модуль работы с VK API:
/*global VK*/
/**
* Function returns Promise for each VK API call.
* Respects the 3 call per second limit.
*
* by Sergei Sokolov <hello@sergeisokolov.com> 2019.
*/
import HorsesHolder from '@/utils/horsesholder';
const debug = true;
const HH = new HorsesHolder({ debug });
export default function asyncVK(methodName, data) {
return HH.add(() => {
data = data || {};
if (!data.v) data.v = 5.92; // VK API version https://vk.com/dev/versions
return new Promise((res, rej) => {
VK.Api.call(
methodName,
data,
r => {
if (r.error) {
debug && console.error("[asyncvk] VK API call error:", r.error);
}
if (r.response) {
res(r.response);
} else if (r.error) {
rej(r.error);
} else {
debug && console.error("[asyncvk] VK API bad response:", r);
rej(r);
}
}
)
});
});
}