How to Promisify Native Xhr

How do I promisify native XHR?

I'm assuming you know how to make a native XHR request (you can brush up here and here)

Since any browser that supports native promises will also support xhr.onload, we can skip all the onReadyStateChange tomfoolery. Let's take a step back and start with a basic XHR request function using callbacks:

function makeRequest (method, url, done) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
done(null, xhr.response);
};
xhr.onerror = function () {
done(xhr.response);
};
xhr.send();
}

// And we'd call it as such:

makeRequest('GET', 'http://example.com', function (err, datums) {
if (err) { throw err; }
console.log(datums);
});

Hurrah! This doesn't involve anything terribly complicated (like custom headers or POST data) but is enough to get us moving forwards.

The promise constructor

We can construct a promise like so:

new Promise(function (resolve, reject) {
// Do some Async stuff
// call resolve if it succeeded
// reject if it failed
});

The promise constructor takes a function that will be passed two arguments (let's call them resolve and reject). You can think of these as callbacks, one for success and one for failure. Examples are awesome, let's update makeRequest with this constructor:

function makeRequest (method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
xhr.send();
});
}

// Example:

makeRequest('GET', 'http://example.com')
.then(function (datums) {
console.log(datums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});

Now we can tap into the power of promises, chaining multiple XHR calls (and the .catch will trigger for an error on either call):

makeRequest('GET', 'http://example.com')
.then(function (datums) {
return makeRequest('GET', datums.url);
})
.then(function (moreDatums) {
console.log(moreDatums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});

We can improve this still further, adding both POST/PUT params and custom headers. Let's use an options object instead of multiple arguments, with the signature:

{
method: String,
url: String,
params: String | Object,
headers: Object
}

makeRequest now looks something like this:

function makeRequest (opts) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(opts.method, opts.url);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
if (opts.headers) {
Object.keys(opts.headers).forEach(function (key) {
xhr.setRequestHeader(key, opts.headers[key]);
});
}
var params = opts.params;
// We'll need to stringify if we've been given an object
// If we have a string, this is skipped.
if (params && typeof params === 'object') {
params = Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
xhr.send(params);
});
}

// Headers and params are optional
makeRequest({
method: 'GET',
url: 'http://example.com'
})
.then(function (datums) {
return makeRequest({
method: 'POST',
url: datums.url,
params: {
score: 9001
},
headers: {
'X-Subliminal-Message': 'Upvote-this-answer'
}
});
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});

A more comprehensive approach can be found at MDN.

Alternatively, you could use the fetch API (polyfill).

xhr promise returns error

I guess the problem is on promise resolving. Try to combine received data into single object and pass it to resolve()

setInterval(function() {
let requestPromise = new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', "location?act=update&id=<?php echo $id; ?>", true);
xhr.send();
xhr.onreadystatechange = function(){
if(this.readyState==4 && this.status == 200 && this.responseText){
try{
var data = JSON.parse(this.responseText);
if(data.command=="list"){
resolve({
answerLat: parseFloat(data.lat),
answerLng: parseFloat(data.lng),
answerAccuracy: data.accuracy,
answerTime: data.time
});
}
}catch(e){
reject(e)
}
}
}
});
requestPromise.then(googleMapsFunction); // googleMapsFunction will be called once the request is completed and will get answerLat as the argument
}, 20000);

const googleMapsFunction = (params) => {
const {answerLat, answerLng, answerAccuracy, answerTime} = params
// ... answerLat, answerLng, answerAccuracy, answerTime
}

How to retry an xhr request which returns a promise recursively for atleast n times on status 0

Here's how I'd approach it (see *** comments):

var makeRequest = function(method, urlToBeCalled, payload) {
var deferred = $q.defer();
var retries = 4; // *** Counter
run(); // *** Call the worker
return deferred.promise;

// *** Move the actual work to its own function
function run() {
var xhr = new XMLHttpRequest();
xhr.open(method, encodeURI(urlToBeCalled), true);
setHttpRequestHeaders(xhr);
xhr.onload = function() {
if (xhr.status === 200 && xhr.readyState === 4 && xhr.getResponseHeader('content-type') !== 'text/html') {
try {
response = JSON.parse(xhr.response);
deferred.resolve(response);
} catch (e) {
deferred.reject(e);
}
} else if (xhr.status === 0) {
// retry
if (retries--) { // *** Recurse if we still have retries
run();
} else {
// *** Out of retries
deferred.reject(e);
}
} else {
// *** See note below, probably remove this
try {
response = JSON.parse(xhr.response);
deferred.reject(response);
} catch (e) {
deferred.reject(xhr.response);
}
}
};
xhr.onerror = function() {
deferred.reject(xhr.response);
};
xhr.send(payload);
}
};

Side note: The content of your initial if body and the final else appear to be identical. I think I'd recast the entire onload:

xhr.onload = function() {
if (xhr.readyState === 4) {
// It's done, what happened?
if (xhr.status === 200) {
if (xhr.getResponseHeader('content-type') !== 'text/html') {
try {
response = JSON.parse(xhr.response);
deferred.resolve(response);
} catch (e) {
deferred.reject(e);
}
} else {
// Something went wrong?
deferred.reject(e);
}
} else if (xhr.status === 0) {
// retry
if (retries--) { // *** Recurse if we still have retries
run();
} else {
// *** Out of retries
deferred.reject(e);
}
}
}
};

Re your comment:

This does resolve my current problem but is there a way to resolve all the promises which are added to call stack if any one of those is resolved?

Yes: To do that with Angular's $q (I assume that's what you're using), you can just pass the promise you get back from the recursive call into resolve on your deferred object: Since it's a promise, the deferred will wait for it to be settled and resolve or reject based on what that promise does. If you do this at every level in the chain, the resolutions work their way up the chain:

angular.module("mainModule", []).controller(

"mainController",

function($scope, $q, $http) {

test(true).then(function() {

test(false);

});

function test(flag) {

log(flag ? "Testing resolved" : "Testing rejected");

return recursive(3, flag)

.then(function(arg) {

log("Resolved with", arg);

})

.catch(function(arg) {

log("Rejected with", arg);

});

}

function recursive(count, flag) {

log("recursive(" + count + ", " + flag + ") called");

var d = $q.defer();

setTimeout(function() {

if (count <= 0) {

// Done, settle

if (flag) {

log("Done, resolving with " + count);

d.resolve(count);

} else {

log("Done, rejecting with " + count);

d.reject(count);

}

} else {

// Not done, resolve with promise from recursive call

log("Not done yet, recursing with " + (count - 1));

d.resolve(recursive(count - 1, flag));

}

}, 0);

return d.promise;

}

}

);

function log() {

var p = document.createElement('pre');

p.appendChild(

document.createTextNode(

Array.prototype.join.call(arguments, " ")

)

);

document.body.appendChild(p);

}
pre {

margin: 0;

padding: 0;

}
<div ng-app="mainModule">

<div ng-controller="mainController"></div>

</div>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

Implement javascript promise or async/await

Return a promise in makeCall so you can wait for it in your main method. Here is an example

const makeCall = (call) => {
return new Promise((resolve, reject) => {
myRequest.open('POST', 'http://111.222.3.444:55555')
myRequest.setRequestHeader('Content-Type', "application/x-www-form-urlencoded");
myRequest.send(call)
myRequest.onload = () => {
if (myRequest.status === 200) {
parseString(myRequest.responseText, (err, result) => {
dataList.unshift(result);
resolve();
})
} else {
console.log('Something went wrong, status code: ' + myRequest.status)
reject();
}
}
});
}

Then you can wait for it to finish to continue in your main method. You can do that by using promises like:

makeCal(...)
.then(() => your_success_logic)
.catch((e) => your_error_logic);

Or you can use async/await like this:

app.get('/home', async (req, res) => {
await makeCal(...);
res.render('home.ejs', {
name: dataList[0].root.slipped.mail.name
});
});

How do I convert an existing callback API to promises?

Promises have state, they start as pending and can settle to:

  • fulfilled meaning that the computation completed successfully.
  • rejected meaning that the computation failed.

Promise returning functions should never throw, they should return rejections instead. Throwing from a promise returning function will force you to use both a } catch { and a .catch. People using promisified APIs do not expect promises to throw. If you're not sure how async APIs work in JS - please see this answer first.

1. DOM load or other one time event:

So, creating promises generally means specifying when they settle - that means when they move to the fulfilled or rejected phase to indicate the data is available (and can be accessed with .then).

With modern promise implementations that support the Promise constructor like native ES6 promises:

function load() {
return new Promise(function(resolve, reject) {
window.onload = resolve;
});
}

You would then use the resulting promise like so:

load().then(function() {
// Do things after onload
});

With libraries that support deferred (Let's use $q for this example here, but we'll also use jQuery later):

function load() {
var d = $q.defer();
window.onload = function() { d.resolve(); };
return d.promise;
}

Or with a jQuery like API, hooking on an event happening once:

function done() {
var d = $.Deferred();
$("#myObject").once("click",function() {
d.resolve();
});
return d.promise();
}

2. Plain callback:

These APIs are rather common since well… callbacks are common in JS. Let's look at the common case of having onSuccess and onFail:

function getUserData(userId, onLoad, onFail) { …

With modern promise implementations that support the Promise constructor like native ES6 promises:

function getUserDataAsync(userId) {
return new Promise(function(resolve, reject) {
getUserData(userId, resolve, reject);
});
}

With libraries that support deferred (Let's use jQuery for this example here, but we've also used $q above):

function getUserDataAsync(userId) {
var d = $.Deferred();
getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
return d.promise();
}

jQuery also offers a $.Deferred(fn) form, which has the advantage of allowing us to write an expression that emulates very closely the new Promise(fn) form, as follows:

function getUserDataAsync(userId) {
return $.Deferred(function(dfrd) {
getUserData(userId, dfrd.resolve, dfrd.reject);
}).promise();
}

Note: Here we exploit the fact that a jQuery deferred's resolve and reject methods are "detachable"; ie. they are bound to the instance of a jQuery.Deferred(). Not all libs offer this feature.

3. Node style callback ("nodeback"):

Node style callbacks (nodebacks) have a particular format where the callbacks is always the last argument and its first parameter is an error. Let's first promisify one manually:

getStuff("dataParam", function(err, data) { …

To:

function getStuffAsync(param) {
return new Promise(function(resolve, reject) {
getStuff(param, function(err, data) {
if (err !== null) reject(err);
else resolve(data);
});
});
}

With deferreds you can do the following (let's use Q for this example, although Q now supports the new syntax which you should prefer):

function getStuffAsync(param) {
var d = Q.defer();
getStuff(param, function(err, data) {
if (err !== null) d.reject(err);
else d.resolve(data);
});
return d.promise;
}

In general, you should not promisify things manually too much, most promise libraries that were designed with Node in mind as well as native promises in Node 8+ have a built in method for promisifying nodebacks. For example

var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only

4. A whole library with node style callbacks:

There is no golden rule here, you promisify them one by one. However, some promise implementations allow you to do this in bulk, for example in Bluebird, converting a nodeback API to a promise API is as simple as:

Promise.promisifyAll(API);

Or with native promises in Node:

const { promisify } = require('util');
const promiseAPI = Object.entries(API).map(([key, v]) => ({key, fn: promisify(v)}))
.reduce((o, p) => Object.assign(o, {[p.key]: p.fn}), {});

Notes:

  • Of course, when you are in a .then handler you do not need to promisify things. Returning a promise from a .then handler will resolve or reject with that promise's value. Throwing from a .then handler is also good practice and will reject the promise - this is the famous promise throw safety.
  • In an actual onload case, you should use addEventListener rather than onX.

Converting Native Promise to JQuery's Promise( For IE )

var p = $.ajax({
url: "yoururl",
type: "GET",
dataType: 'json',
xhrFields: {
withCredentials: true
}
});
// use p.then()

Note:
jQuery has a $.JSON() function directly.

Also, have a look at Promise-Polyfill if you just want promises!



Related Topics



Leave a reply



Submit