Comparing version 0.3.11 to 0.4.0
{ | ||
"name": "popsicle", | ||
"version": "0.3.11", | ||
"version": "0.4.0", | ||
"description": "Simple HTTP requests for node and the browser", | ||
@@ -5,0 +5,0 @@ "main": "popsicle.js", |
585
popsicle.js
/* global define */ | ||
(function () { | ||
(function (root, factory) { | ||
if (typeof define === 'function' && define.amd) { | ||
define([], factory) | ||
} else if (typeof exports === 'object') { | ||
module.exports = factory() | ||
} else { | ||
root.popsicle = factory() | ||
} | ||
})(this, function () { | ||
var isNode = typeof window === 'undefined' | ||
@@ -18,2 +26,4 @@ var root = isNode ? global : window | ||
var abortRequest | ||
var createRequest | ||
var parseRawHeaders | ||
@@ -50,3 +60,2 @@ | ||
* | ||
* @param {Object} obj | ||
* @param {String} property | ||
@@ -56,20 +65,14 @@ * @param {String} callback | ||
*/ | ||
function setProgress (obj, property, callback) { | ||
var method = '_set' + property.charAt(0).toUpperCase() + property.slice(1) | ||
/** | ||
* Create the progress update method. | ||
* | ||
* @param {Number} num | ||
*/ | ||
obj[method] = function (num) { | ||
if (this[property] === num) { | ||
function setProgress (property, callback) { | ||
return function (req, num) { | ||
if (req[property] === num) { | ||
return | ||
} | ||
this[property] = num | ||
req[property] = num | ||
this[callback]() | ||
this._completed() | ||
this._emitProgress() | ||
callback(req) | ||
req.completed = (req.uploaded + req.downloaded) / 2 | ||
emitProgress(req, req._progress) | ||
} | ||
@@ -127,14 +130,2 @@ } | ||
/** | ||
* Create a stream error instance. | ||
* | ||
* @param {Popsicle} self | ||
* @return {Error} | ||
*/ | ||
function streamError (self) { | ||
var err = self.error('Request is streaming') | ||
err.stream = true | ||
return err | ||
} | ||
/** | ||
* Create a timeout error instance. | ||
@@ -146,14 +137,16 @@ * | ||
function abortError (self) { | ||
var timeout = self.timeout | ||
var err | ||
if (self._error) { | ||
return self._error | ||
} | ||
if (self.timedout) { | ||
err = self.error('Timeout of ' + timeout + 'ms exceeded') | ||
err.timeout = timeout | ||
} else { | ||
err = self.error('Request aborted') | ||
err.abort = true | ||
if (!self.timedout) { | ||
var abortedError = self.error('Request aborted') | ||
abortedError.abort = true | ||
return abortedError | ||
} | ||
return err | ||
var timeout = self.timeout | ||
var timedoutError = self.error('Timeout of ' + timeout + 'ms exceeded') | ||
timedoutError.timeout = timeout | ||
return timedoutError | ||
} | ||
@@ -417,3 +410,3 @@ | ||
* @param {Response} response | ||
* @return {Response} | ||
* @return {Promise} | ||
*/ | ||
@@ -437,6 +430,4 @@ function parseResponse (response) { | ||
} catch (e) { | ||
throw parseError(response, e) | ||
return Promise.reject(parseError(response, e)) | ||
} | ||
return response | ||
} | ||
@@ -473,2 +464,26 @@ | ||
/** | ||
* Remove all listener functions. | ||
* | ||
* @param {Request} request | ||
*/ | ||
function removeListeners (request) { | ||
delete request._before | ||
delete request._after | ||
delete request._always | ||
delete request._progress | ||
} | ||
/** | ||
* Check if the request has been aborted before starting. | ||
* | ||
* @param {Request} request | ||
* @return {Promise} | ||
*/ | ||
function checkAborted (request) { | ||
if (request.aborted) { | ||
return Promise.reject(abortError(request)) | ||
} | ||
} | ||
/** | ||
* Set headers on an instance. | ||
@@ -504,2 +519,186 @@ * | ||
/** | ||
* Set the request to error outside the normal request execution flow. | ||
* | ||
* @param {Request} req | ||
* @param {Error} err | ||
*/ | ||
function errored (req, err) { | ||
req._error = err | ||
req.abort() | ||
} | ||
/** | ||
* Emit a request progress event (upload or download). | ||
* | ||
* @param {Array<Function>} fns | ||
*/ | ||
function emitProgress (req, fns) { | ||
if (!fns || req._error) { | ||
return | ||
} | ||
try { | ||
for (var i = 0; i < fns.length; i++) { | ||
fns[i](req) | ||
} | ||
} catch (e) { | ||
errored(req, e) | ||
} | ||
} | ||
/** | ||
* Set upload progress properties. | ||
* | ||
* @type {Function} | ||
* @param {Number} num | ||
*/ | ||
var setUploadSize = setProgress('uploadSize', function (req) { | ||
var n = req.uploaded | ||
var size = req.uploadSize | ||
var total = req.uploadTotal | ||
req.uploaded = calc(n, size, total) | ||
}) | ||
/** | ||
* Set download progress properties. | ||
* | ||
* @type {Function} | ||
* @param {Number} num | ||
*/ | ||
var setDownloadSize = setProgress('downloadSize', function (req) { | ||
var n = req.downloaded | ||
var size = req.downloadSize | ||
var total = req.downloadTotal | ||
req.downloaded = calc(n, size, total) | ||
}) | ||
/** | ||
* Finished uploading. | ||
* | ||
* @param {Request} req | ||
*/ | ||
function setUploadFinished (req) { | ||
if (req.uploaded === 1) { | ||
return | ||
} | ||
req.uploaded = 1 | ||
req.completed = 0.5 | ||
emitProgress(req, req._progress) | ||
} | ||
/** | ||
* Finished downloading. | ||
* | ||
* @param {Request} req | ||
*/ | ||
function setDownloadFinished (req) { | ||
if (req.downloaded === 1) { | ||
return | ||
} | ||
req.downloaded = 1 | ||
req.completed = 1 | ||
emitProgress(req, req._progress) | ||
} | ||
/** | ||
* Create a function for pushing functions onto a stack. | ||
* | ||
* @param {String} prop | ||
* @return {Function} | ||
*/ | ||
function pushListener (prop) { | ||
return function (fn) { | ||
if (this.opened) { | ||
throw new Error('Listeners can not be added after request has started') | ||
} | ||
if (typeof fn !== 'function') { | ||
throw new TypeError('Expected a function but got ' + fn) | ||
} | ||
this[prop] = this[prop] || [] | ||
this[prop].push(fn) | ||
return this | ||
} | ||
} | ||
/** | ||
* Create a promise chain. | ||
* | ||
* @param {Array} fns | ||
* @param {*} arg | ||
* @return {Promise} | ||
*/ | ||
function chain (fns, arg) { | ||
return fns.reduce(function (promise, fn) { | ||
return promise.then(function () { | ||
return fn(arg) | ||
}) | ||
}, Promise.resolve()) | ||
} | ||
/** | ||
* Setup the request instance. | ||
* | ||
* @param {Request} self | ||
*/ | ||
function setup (self) { | ||
var timeout = self.timeout | ||
if (timeout) { | ||
self._timer = setTimeout(function () { | ||
self.timedout = true | ||
self.abort() | ||
}, timeout) | ||
} | ||
// Set the request to "opened", disables any new listeners. | ||
self.opened = true | ||
return chain(self._before, self) | ||
.then(function () { | ||
return createRequest(self) | ||
}) | ||
.then(function (response) { | ||
response.request = self | ||
self.response = response | ||
return chain(self._after, response) | ||
}) | ||
.catch(function (err) { | ||
function reject () { | ||
return Promise.reject(err) | ||
} | ||
return chain(self._always, self).then(reject) | ||
}) | ||
.then(function () { | ||
return chain(self._always, self) | ||
}) | ||
.then(function () { | ||
return self.response | ||
}) | ||
} | ||
/** | ||
* Create the HTTP request promise. | ||
* | ||
* @param {Request} self | ||
* @return {Promise} | ||
*/ | ||
function create (self) { | ||
// Setup a new promise request if none exists. | ||
if (!self._promise) { | ||
self._promise = setup(self) | ||
} | ||
return self._promise | ||
} | ||
/** | ||
* Keep track of headers in a single instance. | ||
@@ -586,11 +785,6 @@ */ | ||
* | ||
* @param {Object} options | ||
* @param {Request} request | ||
*/ | ||
function Response (raw, request) { | ||
function Response (request) { | ||
Headers.call(this) | ||
this.raw = raw | ||
this.request = request | ||
request.response = this | ||
} | ||
@@ -690,2 +884,3 @@ | ||
// Request state. | ||
this.opened = false | ||
this.aborted = false | ||
@@ -702,2 +897,11 @@ | ||
} | ||
this.before(checkAborted) | ||
this.before(defaultAccept) | ||
this.before(stringifyRequest) | ||
this.before(correctType) | ||
this.after(parseResponse) | ||
this.always(removeListeners) | ||
} | ||
@@ -728,3 +932,3 @@ | ||
/** | ||
* Track request completion progress. | ||
* Track various request states. | ||
* | ||
@@ -734,101 +938,8 @@ * @param {Function} fn | ||
*/ | ||
Request.prototype.progress = function (fn) { | ||
if (this.completed) { | ||
return this | ||
} | ||
Request.prototype.before = pushListener('_before') | ||
Request.prototype.after = pushListener('_after') | ||
Request.prototype.always = pushListener('_always') | ||
Request.prototype.progress = pushListener('_progress') | ||
this._progressFns = this._progressFns || [] | ||
this._progressFns.push(fn) | ||
return this | ||
} | ||
/** | ||
* Set upload progress properties. | ||
* | ||
* @private | ||
* @type {Function} | ||
* @param {Number} num | ||
*/ | ||
setProgress(Request.prototype, 'uploadSize', '_uploaded') | ||
setProgress(Request.prototype, 'downloadSize', '_downloaded') | ||
/** | ||
* Calculate the uploaded percentage. | ||
*/ | ||
Request.prototype._uploaded = function () { | ||
var n = this.uploaded | ||
var size = this.uploadSize | ||
var total = this.uploadTotal | ||
this.uploaded = calc(n, size, total) | ||
} | ||
/** | ||
* Calculate the downloaded percentage. | ||
*/ | ||
Request.prototype._downloaded = function () { | ||
var n = this.downloaded | ||
var size = this.downloadSize | ||
var total = this.downloadTotal | ||
this.downloaded = calc(n, size, total) | ||
} | ||
/** | ||
* Update the completed percentage. | ||
*/ | ||
Request.prototype._completed = function () { | ||
this.completed = (this.uploaded + this.downloaded) / 2 | ||
} | ||
/** | ||
* Emit a request progress event (upload or download). | ||
*/ | ||
Request.prototype._emitProgress = function () { | ||
var fns = this._progressFns | ||
if (!fns || this._error) { | ||
return | ||
} | ||
try { | ||
for (var i = 0; i < fns.length; i++) { | ||
fns[i](this) | ||
} | ||
} catch (e) { | ||
this._errored(e) | ||
} | ||
} | ||
/** | ||
* Finished uploading. | ||
*/ | ||
Request.prototype._uploadFinished = function () { | ||
if (this.uploaded === 1) { | ||
return | ||
} | ||
this.uploaded = 1 | ||
this.completed = 0.5 | ||
this._emitProgress() | ||
} | ||
/** | ||
* Finished downloading. | ||
*/ | ||
Request.prototype._downloadFinished = function () { | ||
if (this.downloaded === 1) { | ||
return | ||
} | ||
this.downloaded = 1 | ||
this.completed = 1 | ||
this._emitProgress() | ||
} | ||
/** | ||
* Allows request plugins. | ||
@@ -845,48 +956,2 @@ * | ||
/** | ||
* Setup the request instance (promises and streams). | ||
*/ | ||
Request.prototype._setup = function () { | ||
var self = this | ||
var timeout = this.timeout | ||
this.use(defaultAccept) | ||
this.use(stringifyRequest) | ||
this.use(correctType) | ||
this.progress(function (e) { | ||
if (e.completed === 1) { | ||
delete self._progressFns | ||
} | ||
}) | ||
if (timeout) { | ||
this._timer = setTimeout(function () { | ||
self.timedout = true | ||
self.abort() | ||
}, timeout) | ||
} | ||
} | ||
/** | ||
* Trigger the HTTP request. | ||
* | ||
* @return {Promise} | ||
*/ | ||
Request.prototype.create = function () { | ||
// Setup a new promise request if none exists. | ||
if (!this._promise) { | ||
// If already aborted, create a rejected promise. | ||
if (this.aborted) { | ||
this._promise = Promise.reject(abortError(this)) | ||
} else { | ||
this._setup() | ||
this._promise = this._create().then(parseResponse) | ||
} | ||
} | ||
return this._promise | ||
} | ||
/** | ||
* Abort request. | ||
@@ -907,4 +972,4 @@ * | ||
// Abort and emit the final progress event. | ||
this._abort() | ||
this._emitProgress() | ||
abortRequest(this) | ||
emitProgress(this, this._progress) | ||
clearTimeout(this._timer) | ||
@@ -916,12 +981,2 @@ | ||
/** | ||
* 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. | ||
@@ -946,3 +1001,3 @@ * | ||
cb(null, value) | ||
}, cb) | ||
}).catch(cb) | ||
} | ||
@@ -958,3 +1013,3 @@ | ||
Request.prototype.then = function (onFulfilled, onRejected) { | ||
return this.create().then(onFulfilled, onRejected) | ||
return create(this).then(onFulfilled, onRejected) | ||
} | ||
@@ -965,7 +1020,7 @@ | ||
* | ||
* @param {Function} cb | ||
* @param {Function} onRejected | ||
* @return {Promise} | ||
*/ | ||
Request.prototype['catch'] = function (onRejected) { | ||
return this.create()['catch'](onRejected) | ||
return this.then(null, onRejected) | ||
} | ||
@@ -1040,4 +1095,2 @@ | ||
var trackRequestProgress = function (self, request) { | ||
self._request = request | ||
function onRequest (request) { | ||
@@ -1050,3 +1103,3 @@ var write = request.write | ||
request.write = function (data) { | ||
self._setUploadSize(self.uploadSize + byteLength(data)) | ||
setUploadSize(self, self.uploadSize + byteLength(data)) | ||
@@ -1060,3 +1113,3 @@ return write.apply(this, arguments) | ||
self.downloadTotal = num(response.headers['content-length']) | ||
self._uploadFinished() | ||
setUploadFinished(self) | ||
} | ||
@@ -1066,3 +1119,3 @@ | ||
// Data should always be a `Buffer` instance. | ||
self._setDownloadSize(self.downloadSize + data.length) | ||
setDownloadSize(self, self.downloadSize + data.length) | ||
} | ||
@@ -1101,12 +1154,6 @@ | ||
* | ||
* @param {Request} self | ||
* @return {Promise} | ||
*/ | ||
Request.prototype._create = function () { | ||
var self = this | ||
// Throw on promise creation if streaming. | ||
if (this._stream) { | ||
throw streamError(this) | ||
} | ||
createRequest = function (self) { | ||
return new Promise(function (resolve, reject) { | ||
@@ -1118,3 +1165,3 @@ var opts = requestOptions(self) | ||
delete self._request | ||
self._downloadFinished() | ||
setDownloadFinished(self) | ||
@@ -1130,3 +1177,3 @@ if (err) { | ||
var res = new Response(response, self) | ||
var res = new Response() | ||
@@ -1141,9 +1188,6 @@ res.body = response.body | ||
req.on('abort', function () { | ||
if (self._error) { | ||
return reject(self._error) | ||
} | ||
return reject(abortError(self)) | ||
}) | ||
self._request = req | ||
trackRequestProgress(self, req) | ||
@@ -1156,39 +1200,9 @@ }) | ||
* | ||
* @return {Request} | ||
* @param {Request} self | ||
*/ | ||
Request.prototype._abort = function () { | ||
if (this._request) { | ||
this._request.abort() | ||
abortRequest = function (self) { | ||
if (self._request) { | ||
self._request.abort() | ||
} | ||
} | ||
/** | ||
* Expose the current request stream. | ||
* | ||
* @return {Object} | ||
*/ | ||
Request.prototype.stream = function () { | ||
if (!this._stream) { | ||
this._setup() | ||
// Initialize a streaming request instance. | ||
// TODO: Emit a stream error if already aborted. | ||
// TODO: Catch stream errors and coerce to popsicle errors. | ||
var req = this._stream = request(requestOptions(this)) | ||
trackRequestProgress(this, req) | ||
} | ||
return this._stream | ||
} | ||
/** | ||
* Pipe the current response into another stream. | ||
* | ||
* @param {Object} stream | ||
* @return {Object} | ||
*/ | ||
Request.prototype.pipe = function (stream) { | ||
return this.stream().pipe(stream) | ||
} | ||
} else { | ||
@@ -1240,10 +1254,11 @@ /** | ||
* | ||
* @param {Request} self | ||
* @return {Promise} | ||
*/ | ||
Request.prototype._create = function () { | ||
var self = this | ||
var url = self.fullUrl() | ||
var method = self.method | ||
createRequest = function (self) { | ||
return new Promise(function (resolve, reject) { | ||
var url = self.fullUrl() | ||
var method = self.method | ||
var res = new Response() | ||
return new Promise(function (resolve, reject) { | ||
// Loading HTTP resources from HTTPS is restricted and uncatchable. | ||
@@ -1256,4 +1271,2 @@ if (window.location.protocol === 'https:' && /^http\:/.test(url)) { | ||
var res = new Response(xhr, self) | ||
xhr.onreadystatechange = function () { | ||
@@ -1271,3 +1284,3 @@ if (xhr.readyState === 2) { | ||
// `xhr` object invalid. | ||
self._uploadFinished() | ||
setUploadFinished(self) | ||
} | ||
@@ -1278,8 +1291,4 @@ | ||
delete self._xhr | ||
self._downloadFinished() | ||
setDownloadFinished(self) | ||
if (self._error) { | ||
return reject(self._error) | ||
} | ||
// Handle the aborted state internally, PhantomJS doesn't reset | ||
@@ -1307,3 +1316,3 @@ // `xhr.status` to zero on abort. | ||
self._setDownloadSize(e.loaded) | ||
setDownloadSize(self, e.loaded) | ||
} | ||
@@ -1316,3 +1325,3 @@ | ||
self.uploadTotal = 0 | ||
self._setUploadSize(0) | ||
setUploadSize(self, 0) | ||
} else { | ||
@@ -1324,3 +1333,3 @@ xhr.upload.onprogress = function (e) { | ||
self._setUploadSize(e.loaded) | ||
setUploadSize(self, e.loaded) | ||
} | ||
@@ -1352,6 +1361,8 @@ } | ||
* Abort a running XMLHttpRequest. | ||
* | ||
* @param {Request} self | ||
*/ | ||
Request.prototype._abort = function () { | ||
if (this._xhr) { | ||
this._xhr.abort() | ||
abortRequest = function (self) { | ||
if (self._xhr) { | ||
self._xhr.abort() | ||
} | ||
@@ -1401,3 +1412,3 @@ } | ||
popsicle.jar = function () { | ||
throw new Error('Cookie jars are not supported in browsers') | ||
throw new Error('Cookie jars are not supported on the browser') | ||
} | ||
@@ -1412,11 +1423,3 @@ } | ||
if (typeof define === 'function' && define.amd) { | ||
define([], function () { | ||
return popsicle | ||
}) | ||
} else if (typeof exports === 'object') { | ||
module.exports = popsicle | ||
} else { | ||
root.popsicle = popsicle | ||
} | ||
})() | ||
return popsicle | ||
}) |
@@ -161,5 +161,19 @@ # ![Popsicle](https://cdn.rawgit.com/blakeembrey/popsicle/master/logo.svg) | ||
#### Cookie Jar (Node only) | ||
You can create a reusable cookie jar instance for requests by calling `popsicle.jar`. | ||
```javascript | ||
var jar = request.jar(); | ||
request({ | ||
method: 'POST', | ||
url: '/users', | ||
jar: jar | ||
}); | ||
``` | ||
### Handling Responses | ||
Popsicle responses can be handled in multiple ways. Promises, node-style callbacks and streams (node only) are all supported. | ||
Promises and node-style callbacks are supported. | ||
@@ -195,27 +209,2 @@ #### Promises | ||
#### Streams (Node only) | ||
**Incomplete: Emits incorrect errors** | ||
On node, you can also chain using streams. | ||
```javascript | ||
request('/users') | ||
.pipe(fs.createWriteStream('users.json')); | ||
``` | ||
#### Cookie Jar (Node only) | ||
You can create a reusable cookie jar instance for requests by calling `popsicle.jar`. | ||
```javascript | ||
var jar = request.jar(); | ||
request({ | ||
method: 'POST', | ||
url: '/users', | ||
jar: jar | ||
}); | ||
``` | ||
### Response Objects | ||
@@ -234,2 +223,3 @@ | ||
* **get(key)** Retrieve a HTTP header using a case-insensitive key | ||
* **name(key)** Retrieve the original HTTP header name using a case-insensitive key | ||
* **type()** Return the response type (E.g. `application/json`) | ||
@@ -250,3 +240,3 @@ | ||
A simple plugin interface is exposed through `Request#use` and promises. | ||
A simple plugin interface is exposed through `Request#use`. | ||
@@ -262,10 +252,10 @@ #### Existing Plugins | ||
#### Using Plugins | ||
#### Creating Plugins | ||
Plugins should expose a single function that accepts a `Request` instance. For example: | ||
Plugins must be a function that accepts configuration and returns another function. For example, here's a basic URL prefix plugin. | ||
```javascript | ||
function prefix (uri) { | ||
function prefix (url) { | ||
return function (req) { | ||
req.url = uri + req.url; | ||
req.url = url + req.url; | ||
}; | ||
@@ -281,2 +271,8 @@ } | ||
If you need to augment the request or response lifecycle, there are a number of functions you can register. All listeners accept an optional promise that will resolve before proceeding. | ||
* **before(fn)** Register a function to run before the request is made | ||
* **after(fn)** Register a function to receive the response object | ||
* **always(fn)** Register a function that always runs on `resolve` or `reject` | ||
## Development and Testing | ||
@@ -286,3 +282,3 @@ | ||
```bash | ||
``` | ||
npm install && npm test | ||
@@ -293,5 +289,5 @@ ``` | ||
* [Superagent](https://github.com/visionmedia/superagent) - HTTP requests on node and browser | ||
* [Superagent](https://github.com/visionmedia/superagent) - HTTP requests for node and browsers | ||
* [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 | ||
* [Axios](https://github.com/mzabriskie/axios) - HTTP request API based on Angular's $http service | ||
@@ -298,0 +294,0 @@ ## License |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
47063
1175
0
296