Comparing version 0.1.1 to 0.2.0
{ | ||
"name": "popsicle", | ||
"main": "popsicle.js", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"homepage": "https://github.com/blakeembrey/popsicle", | ||
@@ -6,0 +6,0 @@ "authors": [ |
{ | ||
"name": "popsicle", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Simple HTTP requests for node and the browser", | ||
@@ -5,0 +5,0 @@ "main": "popsicle.js", |
276
popsicle.js
@@ -41,2 +41,45 @@ (function (root) { | ||
/** | ||
* Create a function to set progress properties on a request instance. | ||
* | ||
* @param {String} property | ||
* @return {Function} | ||
*/ | ||
function setProgress (property) { | ||
return function (num) { | ||
if (this[property] === num) { | ||
return; | ||
} | ||
this[property] = num; | ||
this._emitProgress(); | ||
}; | ||
} | ||
/** | ||
* Calculate the length of the current request as a percent of the total. | ||
* | ||
* @param {Number} length | ||
* @param {Number} total | ||
* @return {Number} | ||
*/ | ||
function calc (length, total) { | ||
if (total == null) { | ||
return 0; | ||
} | ||
return total === length ? 1 : length / total; | ||
} | ||
/** | ||
* Return zero if the number is `Infinity`. | ||
* | ||
* @param {Number} num | ||
* @return {Number} | ||
*/ | ||
function infinity (num) { | ||
return num === Infinity ? 0 : num; | ||
} | ||
/** | ||
* Create a stream error instance. | ||
@@ -473,15 +516,53 @@ * | ||
* @param {Request} self | ||
* @param {request} request | ||
*/ | ||
function trackResponseProgress (self) { | ||
self._request.on('response', function (res) { | ||
self._responseTotal = Number(res.headers['content-length']); | ||
}); | ||
function trackRequestProgress (self, request) { | ||
var write = request.write; | ||
self._request.on('data', function (data) { | ||
self._responseLength += data.length; | ||
}); | ||
self._request = request; | ||
self._request.on('end', function () { | ||
self._responseTotal = self._responseLength; | ||
}); | ||
// Override `Request.prototype.write` to track written data. | ||
request.write = function (data) { | ||
self._setRequestLength(self._requestLength + data.length); | ||
return write.apply(request, arguments); | ||
}; | ||
function onRequestEnd () { | ||
self._setRequestTotal(self._requestLength); | ||
} | ||
function onRequest () { | ||
self._setRequestTotal(Number(request.headers['content-length']) || 0); | ||
request.req.on('finish', onRequestEnd); | ||
} | ||
function onResponse (response) { | ||
self._responseTotal = Number(response.headers['content-length']); | ||
} | ||
function onResponseData (data) { | ||
self._setResponseLength(self._responseLength + data.length); | ||
} | ||
function onResponseEnd () { | ||
self._setResponseTotal(self._responseLength); | ||
removeListeners(); | ||
self._completed(); | ||
} | ||
function removeListeners () { | ||
request.removeListener('request', onRequest); | ||
request.removeListener('response', onResponse); | ||
request.removeListener('data', onResponseData); | ||
request.removeListener('end', onResponseEnd); | ||
request.req.removeListener('finish', onRequestEnd); | ||
} | ||
request.on('request', onRequest); | ||
request.on('response', onResponse); | ||
request.on('data', onResponseData); | ||
request.on('end', onResponseEnd); | ||
request.on('error', removeListeners); | ||
} | ||
@@ -702,4 +783,6 @@ | ||
// Request options. | ||
this.body = options.body; | ||
this.url = options.url; | ||
this.method = (options.method || 'GET').toUpperCase(); | ||
this.query = assign({}, options.query); | ||
@@ -710,6 +793,5 @@ this.timeout = options.timeout; | ||
// Default to GET and uppercase anything else. | ||
this.method = (options.method || 'GET').toUpperCase(); | ||
// Initialize the response length. | ||
// Progress properties. | ||
this._requestTotal = null; | ||
this._requestLength = 0; | ||
this._responseTotal = null; | ||
@@ -721,2 +803,6 @@ this._responseLength = 0; | ||
// Request state. | ||
this.aborted = false; | ||
this.completed = false; | ||
// Parse query strings already set. | ||
@@ -754,16 +840,96 @@ var queryIndex = options.url.indexOf('?'); | ||
/** | ||
* Check how far something has been downloaded. | ||
* Check how far the request has been uploaded. | ||
* | ||
* @return {Number} | ||
*/ | ||
Request.prototype.uploaded = function () { | ||
return calc(this._requestLength, this._requestTotal); | ||
}; | ||
/** | ||
* Check how far the request has been downloaded. | ||
* | ||
* @return {Number} | ||
*/ | ||
Request.prototype.downloaded = function () { | ||
if (this._responseTotal == null) { | ||
return 0; | ||
return calc(this._responseLength, this._responseTotal); | ||
}; | ||
/** | ||
* Track request completion progress. | ||
* | ||
* @param {Function} fn | ||
* @return {Request} | ||
*/ | ||
Request.prototype.progress = function (fn) { | ||
if (this.completed) { | ||
return this; | ||
} | ||
return this._responseLength / this._responseTotal; | ||
this._progressFns = this._progressFns || []; | ||
this._progressFns.push(fn); | ||
return this; | ||
}; | ||
/** | ||
* Set various progress properties. | ||
* | ||
* @private | ||
* @param {Number} num | ||
*/ | ||
Request.prototype._setRequestTotal = setProgress('_requestTotal'); | ||
Request.prototype._setRequestLength = setProgress('_requestLength'); | ||
Request.prototype._setResponseTotal = setProgress('_responseTotal'); | ||
Request.prototype._setResponseLength = setProgress('_responseLength'); | ||
/** | ||
* Emit a request progress event (upload or download). | ||
*/ | ||
Request.prototype._emitProgress = function () { | ||
var fns = this._progressFns; | ||
if (!fns) { | ||
return; | ||
} | ||
var self = this; | ||
var aborted = this.aborted; | ||
var uploaded = this.uploaded(); | ||
var downloaded = this.downloaded(); | ||
var total = (infinity(uploaded) + infinity(downloaded)) / 2; | ||
function emitProgress () { | ||
try { | ||
fns.forEach(function (fn) { | ||
// Emit new progress object every iteration to avoid issues if a | ||
// function mutates the object. | ||
fn({ | ||
uploaded: uploaded, | ||
downloaded: downloaded, | ||
total: total, | ||
aborted: aborted | ||
}); | ||
}); | ||
} catch (e) { | ||
self._errored(e); | ||
} | ||
} | ||
emitProgress(); | ||
}; | ||
/** | ||
* Complete the request. | ||
*/ | ||
Request.prototype._completed = function () { | ||
this.completed = true; | ||
delete this._progressFns; | ||
}; | ||
/** | ||
* Allows request plugins. | ||
* | ||
* @return {Request} | ||
*/ | ||
@@ -777,5 +943,3 @@ Request.prototype.use = function (fn) { | ||
/** | ||
* Setup and create the request instance. | ||
* | ||
* @return {Promise} | ||
* Setup the request instance (promises and streams). | ||
*/ | ||
@@ -812,3 +976,2 @@ Request.prototype._setup = function () { | ||
// Promises buffer and parse the full response. | ||
this._promise = this._create().then(parseResponse); | ||
@@ -832,3 +995,12 @@ } | ||
this.aborted = true; | ||
// Reset progress and complete progress events. | ||
this._requestTotal = 0; | ||
this._requestLength = 0; | ||
this._responseTotal = 0; | ||
this._responseLength = 0; | ||
this._emitProgress(); | ||
this._abort(); | ||
this._completed(); | ||
clearTimeout(this._timer); | ||
@@ -840,2 +1012,12 @@ | ||
/** | ||
* Trigger a request-related error that should break requests. | ||
* | ||
* @param {Error} err | ||
*/ | ||
Request.prototype._errored = function (err) { | ||
this._error = err; | ||
this.abort(); | ||
}; | ||
/** | ||
* Create a popsicle error instance. | ||
@@ -906,5 +1088,10 @@ * | ||
self._request = request(opts, function (err, response) { | ||
var req = request(opts, function (err, response) { | ||
if (err) { | ||
return reject(unavailableError(self)); | ||
// Node.js core error (ECONNRESET, EPIPE). | ||
if (typeof err.code === 'string') { | ||
return reject(unavailableError(self)); | ||
} | ||
return reject(err); | ||
} | ||
@@ -923,7 +1110,11 @@ | ||
self._request.on('abort', function () { | ||
req.on('abort', function () { | ||
if (self._error) { | ||
return reject(self._error); | ||
} | ||
return reject(abortError(self)); | ||
}); | ||
trackResponseProgress(self); | ||
trackRequestProgress(self, req); | ||
}); | ||
@@ -978,4 +1169,5 @@ }; | ||
Request.prototype._create = function ( ) { | ||
var self = this; | ||
var url = this.fullUrl(); | ||
var self = this; | ||
var url = self.fullUrl(); | ||
var method = self.method; | ||
@@ -998,3 +1190,3 @@ return new Promise(function (resolve, reject) { | ||
if (xhr.readyState === 3) { | ||
self._responseLength = xhr.responseText.length; | ||
self._setResponseLength(xhr.responseText.length); | ||
} | ||
@@ -1005,4 +1197,13 @@ | ||
// in case the content length header was not available before. | ||
self._responseTotal = self._responseLength; | ||
self._setResponseTotal(self._responseLength); | ||
// Clean up listeners. | ||
delete xhr.onreadystatechange; | ||
delete xhr.upload.onprogress; | ||
self._completed(); | ||
if (self._error) { | ||
return reject(self._error); | ||
} | ||
// Handle the aborted state internally, PhantomJS doesn't reset | ||
@@ -1030,5 +1231,18 @@ // `xhr.status` to zero on abort. | ||
// No upload will occur with these requests. | ||
if (method === 'GET' || method === 'HEAD' || !xhr.upload) { | ||
xhr.upload = {}; | ||
self._setRequestTotal(0); | ||
self._setRequestLength(0); | ||
} else { | ||
xhr.upload.onprogress = function (e) { | ||
self._setRequestTotal(e.total); | ||
self._setRequestLength(e.loaded); | ||
}; | ||
} | ||
// XHR can fail to open when site CSP is set. | ||
try { | ||
xhr.open(self.method, url); | ||
xhr.open(method, url); | ||
} catch (e) { | ||
@@ -1035,0 +1249,0 @@ return reject(cspError(self, e)); |
@@ -6,3 +6,3 @@ # ![Popsicle](https://cdn.rawgit.com/blakeembrey/popsicle/master/logo.svg) | ||
**Popsicle** is designed to be easiest way for making HTTP requests, offering a consistant and intuitive API that works on both node and the browser. | ||
**Popsicle** is designed to be easiest way for making HTTP requests, offering a consistent and intuitive API that works on both node and the browser. | ||
@@ -20,2 +20,3 @@ ```javascript | ||
npm install popsicle --save | ||
bower install popsicle --save | ||
``` | ||
@@ -26,3 +27,4 @@ | ||
```bash | ||
npm install es6-promise | ||
npm install es6-promise --save | ||
bower install es6-promise --save | ||
``` | ||
@@ -123,11 +125,22 @@ | ||
#### Download Progress | ||
#### Progress | ||
The request object can also be used to check progress at any time. However, the URL must have responded with a `Content-Length` header for this to work properly. | ||
The request object can also be used to check progress at any time. | ||
`Request#uploaded` can be used to check the upload progress (between `0` and `1`) of the current request. If the request is being streamed (node), the length may incorrectly return `Infinity` to signify the number can't be calculated. | ||
`Request#downloaded` can be used to check the download progress (between `0` and `1`) of the current request instance. If the response did not respond with a `Content-Length` header this can not be calculated properly and will return `Infinity`. | ||
`Request#progress(fn)` can be used to register a progress event listener. It'll emit on upload and download progress events, which makes it useful for SPA progress bars. The function is called with an object (`{ uploaded: 0, downloaded: 0, total: 0, aborted: false }`). | ||
```javascript | ||
var req = request('http://example.com'); | ||
req.uploaded(); //=> 0 | ||
req.downloaded(); //=> 0 | ||
req.progress(function (e) { | ||
console.log(e); //=> { uploaded: 1, downloaded: 0, total: 0.5, aborted: false } | ||
}); | ||
req.then(function (res) { | ||
@@ -173,4 +186,6 @@ console.log(req.downloaded()); //=> 1 | ||
On node, you can also chain using streams. However, the API is currently incomplete. | ||
**Incomplete: Emits incorrect errors** | ||
On node, you can also chain using streams. | ||
```javascript | ||
@@ -209,7 +224,10 @@ request('/users') | ||
A simple plugin interface is exposed through `Request#use`. | ||
A simple plugin interface is exposed through `Request#use` and promises. | ||
#### Existing Plugins | ||
None yet. Will you be the first? | ||
* [Status](https://github.com/blakeembrey/popsicle-status) - Reject responses on HTTP failure status codes | ||
* [No Cache](https://github.com/blakeembrey/popsicle-no-cache) - Prevent caching of HTTP requests | ||
* [Basic Auth](https://github.com/blakeembrey/popsicle-basic-auth) - Add basic authentication to requests | ||
* [Prefix](https://github.com/blakeembrey/popsicle-prefix) - Automatically prefix all HTTP requests | ||
@@ -242,2 +260,8 @@ #### Using Plugins | ||
## Related Projects | ||
* [Superagent](https://github.com/visionmedia/superagent) - HTTP requests on node and browser | ||
* [Fetch](https://github.com/github/fetch) - Browser polyfill for promise-based HTTP requests | ||
* [Axios](https://github.com/mzabriskie/axios) - Similar API based on Angular's $http service | ||
## License | ||
@@ -244,0 +268,0 @@ |
@@ -329,2 +329,4 @@ var REMOTE_URL = 'http://localhost:4567'; | ||
expect(err.message).to.equal('Request aborted'); | ||
expect(err.abort).to.be.true; | ||
expect(err.popsicle).to.be.an.instanceOf(popsicle.Request); | ||
}) | ||
@@ -337,24 +339,106 @@ .then(function () { | ||
describe('download progress', function () { | ||
it('should check download progress', function () { | ||
var req = popsicle(REMOTE_URL + '/download'); | ||
var assert = false; | ||
describe('progress', function () { | ||
describe('download', function () { | ||
it('should check download progress', function () { | ||
var req = popsicle(REMOTE_URL + '/download'); | ||
var assert = false; | ||
// Before the request has started. | ||
expect(req.downloaded()).to.equal(0); | ||
// Before the request has started. | ||
expect(req.downloaded()).to.equal(0); | ||
// Check halfway into the response. | ||
setTimeout(function () { | ||
assert = req.downloaded() === 0.5; | ||
}, 100); | ||
// Check halfway into the response. | ||
setTimeout(function () { | ||
assert = req.downloaded() === 0.5; | ||
}, 100); | ||
return req.then(function () { | ||
// Can't consistently test progress in browsers. | ||
if (typeof window === 'undefined') { | ||
expect(assert).to.be.true; | ||
} | ||
return req | ||
.then(function () { | ||
// Can't consistently test progress in browsers. | ||
if (typeof window === 'undefined') { | ||
expect(assert).to.be.true; | ||
} | ||
expect(req.downloaded()).to.equal(1); | ||
expect(req.downloaded()).to.equal(1); | ||
}); | ||
}); | ||
}); | ||
describe('event', function () { | ||
it('should emit progress events', function () { | ||
var req = popsicle({ | ||
url: REMOTE_URL + '/echo', | ||
body: EXAMPLE_BODY, | ||
method: 'POST' | ||
}); | ||
var asserted = 0; | ||
var expected = 0; | ||
req.progress(function (e) { | ||
// Fix for PhantomJS tests (doesn't return `Content-Length` header). | ||
if (req._xhr && e.downloaded === Infinity) { | ||
console.warn('Browser does not support "Content-Length" header'); | ||
return; | ||
} | ||
expect(e.total).to.equal(expected); | ||
asserted += 1; | ||
expected += 0.5; | ||
}); | ||
return req | ||
.then(function (res) { | ||
expect(asserted).to.equal(3); | ||
expect(res.body).to.deep.equal(EXAMPLE_BODY); | ||
}); | ||
}); | ||
it('should error when the progress callback errors', function () { | ||
var req = popsicle(REMOTE_URL + '/echo'); | ||
var errored = false; | ||
req.progress(function () { | ||
throw new Error('Testing'); | ||
}); | ||
return req | ||
.catch(function (err) { | ||
errored = true; | ||
expect(err.message).to.equal('Testing'); | ||
expect(err.popsicle).to.not.exist; | ||
}) | ||
.then(function () { | ||
expect(errored).to.be.true; | ||
}); | ||
}); | ||
it('should emit a final event on abort', function () { | ||
var req = popsicle(REMOTE_URL + '/echo'); | ||
var errored = false; | ||
var progressed = 0; | ||
req.progress(function (e) { | ||
expect(e.total).to.equal(1); | ||
expect(e.aborted).to.be.true; | ||
progressed++; | ||
}); | ||
req.abort(); | ||
return req | ||
.catch(function (err) { | ||
errored = true; | ||
expect(err.abort).to.be.true; | ||
}) | ||
.then(function () { | ||
expect(errored).to.be.true; | ||
expect(progressed).to.equal(1); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -361,0 +445,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
68157
1821
268