Skip to content

Commit

Permalink
Merge pull request #31 from FamilySearch/interval
Browse files Browse the repository at this point in the history
Implement requestInterval middleware; close #30
  • Loading branch information
justincy authored Aug 17, 2017
2 parents b13c656 + 6a7ff91 commit 0a8bcbd
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 14 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,14 @@ var fs = new FamilySearch({
maxThrottledRetries: 10,

// List of pending modifications that should be activated.
pendingModifications: ['consolidate-redundant-resources', 'another-pending-mod']
pendingModifications: ['consolidate-redundant-resources', 'another-pending-mod'],

// Optional settings that enforces a minimum time in milliseconds (ms) between
// requests. This is useful for smoothing out bursts of requests and being nice
// to the API servers. When this parameter isn't set (which is the default)
// then all requests are immediately sent.
requestInterval: 1000

});
```

Expand Down
23 changes: 10 additions & 13 deletions src/FamilySearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,7 @@ var cookies = require('doc-cookies'),
/**
* Create an instance of the FamilySearch SDK Client
*
* @param {Object} options
* @param {String} options.environment Reference environment: production, beta,
* or integration. Defaults to integration.
* @param {String} options.appKey Application Key
* @param {String} options.redirectUri OAuth2 redirect URI
* @param {String} options.saveAccessToken Save the access token to a cookie
* and automatically load it from that cookie. Defaults to false.
* @param {String} options.tokenCookie Name of the cookie that the access token
* will be saved in when `saveAccessToken` is true. Defaults to 'FS_AUTH_TOKEN'.
* @param {String} options.maxThrottledRetries Maximum number of a times a
* throttled request should be retried. Defaults to 10.
* @param {Array} options.pendingModifications List of pending modifications
* that should be activated.
* @param {Object} options See a description of the possible options in the docs for config().
*/
var FamilySearch = function(options){

Expand Down Expand Up @@ -67,6 +55,11 @@ var FamilySearch = function(options){
* throttled request should be retried. Defaults to 10.
* @param {Array} options.pendingModifications List of pending modifications
* that should be activated.
* @param {Integer} options.requestInterval Minimum interval between requests in milliseconds (ms).
* By default this behavior is disabled; i.e. requests are issued immediately.
* When this option is set then requests are queued to ensure there is at least
* {requestInterval} ms between them. This is useful for smoothing out bursts
* of requests and thus playing nice with the API servers.
*/
FamilySearch.prototype.config = function(options){
this.appKey = options.appKey || this.appKey;
Expand All @@ -84,6 +77,10 @@ FamilySearch.prototype.config = function(options){
this.addRequestMiddleware(requestMiddleware.pendingModifications(options.pendingModifications));
}

if(parseInt(options.requestInterval, 10)) {
this.addRequestMiddleware(requestMiddleware.requestInterval(parseInt(options.requestInterval, 10)));
}

// When the SDK is configured to save the access token in a cookie and we don't
// presently have an access token then we try loading one from the cookie.
//
Expand Down
1 change: 1 addition & 0 deletions src/middleware/request/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
defaultAcceptHeader: require('./defaultAcceptHeader'),
disableAutomaticRedirects: require('./disableAutomaticRedirects'),
pendingModifications: require('./pendingModifications'),
requestInterval: require('./requestInterval'),
url: require('./url')
};
80 changes: 80 additions & 0 deletions src/middleware/request/requestInterval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Enforce a minimum interval between requests.
* See https://github.com/FamilySearch/fs-js-lite/issues/30
*
* @param {Integer} interval Minimum time between requests, in milliseconds (ms)
* @return {Function} middleware
*/
module.exports = function(interval) {

var lastRequestTime = 0,
requestQueue = [],
timer;

/**
* Add a request to the queue
*
* @param {Function} next The next method that was sent to the middleware with the request.
*/
function enqueue(next) {
requestQueue.push(next);
startTimer();
}

/**
* Start the timer that checks for when a request in the queue is ready to go.
* This fires every {interval} ms to enforce a minimum time between requests.
* The actual time between requests may be longer.
*/
function startTimer() {
if(!timer) {
timer = setInterval(checkQueue, interval);
}
}

/**
* Check to see if we're ready to send any requests.
*/
function checkQueue() {
if(!inInterval()) {
if(requestQueue.length) {
var next = requestQueue.shift();
sendRequest(next);
} else if(timer) {
clearInterval(timer); // No need to leave the timer running if we don't have any requests.
}
}
}

/**
* Send a request by calling it's next() method and mark the current time so
* that we can accurately enforce the interval.
*/
function sendRequest(next) {
lastRequestTime = Date.now();
next();
}

/**
* Returns true if the most recent request was less then {interval} ms in the past
*
* @return {Boolean}
*/
function inInterval() {
return Date.now() - lastRequestTime < interval;
}

return function(client, request, next) {

// If there are any requests in the queue or if the previous request was issued
// too recently then add this request to the queue.
if(requestQueue.length || inInterval()) {
enqueue(next);
}

else {
sendRequest(next);
}
};

};
45 changes: 45 additions & 0 deletions test/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,51 @@ describe('node', function(){

});

describe('requestInterval', function(){

it('request interval is enforced', function(done){
// Here we test that requests are propertly enqueued and the timing
// between them is enforced when the requestInterval param is present.
var interval = 1500,
numRequests = 3,
timings = [],
client = apiClient({
requestInterval: interval
});
this.timeout(interval * numRequests * 2);
client.addRequestMiddleware(function(client, request, next){
// This middleware is added after the requestInterval middleware so we
// know that it's called after the requestInterval limit has been enforced.
timings[request.options.num] = Date.now();
if(timings.length === numRequests) {
done(verifyTimings());
}
next();
});
for(var i = 0; i < numRequests; i++) {
client.get('/foo', {
num: i
}, function(){});
}

// Make sure each subsequent request was at least {interval} ms after the
// previous request. Our timing is marked on a different tick in this test
// than it is in the middleware and that can account for many ms of
// difference so we allow a shortcoming of 30 ms below.
function verifyTimings() {
var diff;
for(var i = 1; i < numRequests; i++) {
diff = timings[i] - timings[i-1];
if(diff < interval - 30) {
console.log(timings);
return new Error(`Request ${i+1} was only ${diff}ms after the request before it; expected ${interval}ms.`);
}
}
}
});

});

});

/**
Expand Down

0 comments on commit 0a8bcbd

Please sign in to comment.