UNPKG

heroku-client

Version:

A wrapper for the Heroku v3 API

300 lines (237 loc) 7.08 kB
var https = require('https'), agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 }), encryptor = require('./encryptor'), memjs = require('memjs'), q = require('q'), cache; module.exports = Request; /* * Create an object capable of making API * calls. Accepts custom request options and * a callback function. */ function Request (options, callback) { this.options = options || {}; this.callback = callback; this.deferred = q.defer(); this.nextRange = 'id ]..; max=1000'; } /* * Instantiate a Request object and makes a * request, returning the request promise. */ Request.request = function request (options, callback) { var req = new Request(options, function (err, body) { if (callback) callback(err, body); }); return req.request(); }; /* * Check for a cached response, then * perform an API request. Return the * request object's promise. */ Request.prototype.request = function request () { this.getCache(this.performRequest.bind(this)); return this.deferred.promise; }; /* * Perform the actual API request. */ Request.prototype.performRequest = function performRequest (cachedResponse) { var headers, key, requestOptions, req; this.cachedResponse = cachedResponse; headers = { 'Accept': 'application/vnd.heroku+json; version=3', 'Content-type': 'application/json', 'Range': this.nextRange }; this.options.headers || (this.options.headers = {}); for (key in this.options.headers) { headers[key] = this.options.headers[key]; } if (this.cachedResponse) { headers['If-None-Match'] = this.cachedResponse.etag; } requestOptions = { agent: agent, hostname: 'api.heroku.com', port: 443, path: this.options.path, auth: ':' + this.options.token, method: this.options.method || 'GET', headers: headers }; req = https.request(requestOptions, this.handleResponse.bind(this)); this.writeBody(req); this.setRequestTimeout(req); req.on('error', this.handleError.bind(this)); req.end(); }; /* * Handle an API response, returning the * cached body if it's still valid, or the * new API response. */ Request.prototype.handleResponse = function handleResponse (res) { var _this = this, buffer; if (res.statusCode === 304 && this.cachedResponse) { if (this.cachedResponse.nextRange) { this.nextRequest(this.cachedResponse.nextRange, this.cachedResponse.body); } else { this.updateAggregate(this.cachedResponse.body); this.deferred.resolve(this.aggregate); this.callback(null, this.aggregate); } } else { buffer = ''; res.on('data', function (data) { buffer += data; }); res.on('end', function () { if (res.statusCode.toString().match(/^2\d{2}$/)) { _this.handleSuccess(res, buffer); } else { _this.handleFailure(res, buffer); } }); } }; /* * If the request options include a body, * write the body to the request and set * an appropriate 'Content-length' header. */ Request.prototype.writeBody = function writeBody (req) { if (!this.options.body) return; var body = JSON.stringify(this.options.body); req.setHeader('Content-length', body.length); req.write(body); } /* * If the request options include a timeout, * set the timeout and provide a callback * function in case the request exceeds the * timeout period. */ Request.prototype.setRequestTimeout = function setRequestTimeout (req) { var _this = this; if (!this.options.timeout) return; req.setTimeout(this.options.timeout, function () { var err = new Error('Request took longer than ' + _this.options.timeout + 'ms to complete.'); req.abort(); _this.deferred.reject(err); _this.callback(err); }); } /* * In the event of an error in performing * the API request, reject the deferred * object and return an error to the callback. */ Request.prototype.handleError = function handleError (err) { this.deferred.reject(err); this.callback(err); } /* * In the event of a non-successful API request, * fail with an appropriate error message and * status code. */ Request.prototype.handleFailure = function handleFailure (res, buffer) { var options = this.options, callback = this.callback, deferred = this.deferred, message = 'Expected response to be successful, got ' + res.statusCode, err; err = new Error(message); err.statusCode = res.statusCode; err.body = JSON.parse(buffer || "{}"); deferred.reject(err); callback(err); } /* * In the event of a successful API response, * write the response to the cache and resolve * with the response body. */ Request.prototype.handleSuccess = function handleSuccess (res, buffer) { var options = this.options, callback = this.callback, deferred = this.deferred, body = JSON.parse(buffer || '{}'); this.setCache(res, body); if (res.headers['next-range']) { this.nextRequest(res.headers['next-range'], body); } else { this.updateAggregate(body); deferred.resolve(this.aggregate); callback(null, this.aggregate); } } /* * Since this request isn't the full response (206 or * 304 with a cached Next-Range), perform the next * request for more data. */ Request.prototype.nextRequest = function nextRequest (nextRange, body) { this.updateAggregate(body); this.nextRange = nextRange; this.request(); } /* * If the cache client is alive, get the * cached response from the cache. */ Request.prototype.getCache = function getCache (callback) { if (!cache) return callback(null); var key = this.getCacheKey(); cache.get(key, function (err, res) { res = res ? encryptor.decrypt(res.toString()) : res; callback(JSON.parse(res)); }); }; /* * If the cache client is alive, write the * provided response and body to the cache. */ Request.prototype.setCache = function setCache (res, body) { if ((!cache) || !(res.headers.etag)) return; var key = this.getCacheKey(); var value = JSON.stringify({ body: body, etag: res.headers.etag, nextRange: res.headers['next-range'] }); value = encryptor.encrypt(value); cache.set(key, value); } /* * Returns a cache key comprising the request path, * the 'Next Range' header, and the user's API token. */ Request.prototype.getCacheKey = function getCacheKey () { return encryptor.encrypt(this.options.path + this.nextRange + this.options.token); }; /* * If given an object, sets aggregate to object, * otherwise concats array onto aggregate. */ Request.prototype.updateAggregate = function updateAggregate (aggregate) { if (aggregate instanceof Array) { this.aggregate || (this.aggregate = []); this.aggregate = this.aggregate.concat(aggregate); } else { this.aggregate = aggregate; } } /* * Connect a cache client. */ Request.connectCacheClient = function connectCacheClient() { cache = memjs.Client.create(); };