Cleaning up Spaghetti Code w/ Promises - javascript

In one of my beforeSave functions, I need to check a number of conditions before responding with success or error.
The problem is, my code seems really messy and sometimes success/error isn't called:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
entry.get("user").fetch().then(function(fetchedUser) {
contest.fetch().then(function(fetchedContest) {
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") ) {
response.error('Insufficient Funds.');
} else {
fetchedContest.get("timeSlot").fetch().then(function(fetchedTimeSlot) {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now) {
response.error('This contest has already started.');
} else {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
contest.save().then(function(fetchedContest) {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
} else {
response.success();
}
});
}
});
}
});
});
});
I've been trying to learn promises to tidy this up, and here was my (failed) attempt:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
entry.get("user").fetch().then(function(fetchedUser) {
contest.fetch().then(function(fetchedContest) {
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") ) {
response.error('Insufficient Funds.');
}
return fetchedContest;
});
}).then(function(result) {
result.get("timeSlot").fetch().then(function(fetchedTimeSlot) {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now) {
response.error('This contest has already started.');
}
});
}).then(function() {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0);
contest.save().then(function(fetchedContest) {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
}
});
}).then(function() {
response.success();
}, function(error) {
response.error(error);
});
});
Any help or examples on how to use promises for this would be much appreciated. Clearly I do not fully understand how they work syntactically yet.

I cleaned it up a bit by getting the fetched variables assembled first using a chain of promises. This follows a couple style rules that make it more readable (for me anyway)...
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
var fetchedUser, fetchedContest;
var errorMessage;
entry.get("user").fetch().then(function(result) {
fetchedUser = result;
return contest.fetch();
}).then(function(result) {
fetchedContest = result;
return fetchedContest.get("timeSlot").fetch();
}).then(function(fetchedTimeSlot) {
// now we have all the variables we need to determine validity
var now = new Date();
var hasSufficientFunds = fetchedUser.get("fundsAvailable") >= fetchedContest.get("entryFee");
var contestNotStarted = fetchedTimeSlot.get("startDate") >= now;
if (hasSufficientFunds && contestNotStarted) {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
return contest.save();
} else {
errorMessage = (hasSufficientFunds)? 'This contest has already started.' : 'Insufficient Funds.';
return null;
}
}).then(function(result) {
if (!result) {
response.error(errorMessage);
} else {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
} else {
response.success();
}
}
}, function(error) {
response.error(error);
});
});
Note how we never leave a response.anything() dangling, we always make clear what's flowing to the next promise via a return.

You are doing the rookie mistake of not returning promises inside .then while chaining them, other than that your chain would still continue even when you call response.error(.., simplest way to break chain is throwing an error, your code can be flattened as:
Parse.Cloud.beforeSave("Entry", (request, response) => {
var entry = request.object,
, contest = request.object.get("contest")
, fetchedUser
, fetchedContest;
entry.get("user").fetch()
.then(_fetchedUser => {
fetchedUser = _fetchedUser;
return contest.fetch();
}).then(_fetchedContest => {
fetchedContest = _fetchedContest;
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") )
return Promise.reject('Insufficient Funds.');
return fetchedContest.get("timeSlot").fetch();
}).then(fetchedTimeSlot => {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now)
return Promise.reject('This contest has already started.');
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
return contest.save();
}).then(fetchedContest => {
if (contest.get("entriesCount") > contest.get("entriesLimit"))
return Promise.reject('The contest is full.');
response.success();
}).catch(response.error.bind(response));
});

Related

Pouchdb wait for promise inside promise

The following code does not give the expected output:
function downloadData() {
var timeStamp = new Date();
db.upsert("timeData", function(doc) {
if (!doc.nextRun) {
doc.nextRun = timeStamp.getTime();
}
if (doc.nextRun < timeStamp.getTime()) {
console.log("yes")
timeStamp.setSeconds(timeStamp.getSeconds() + 10);
doc.nextRun = timeStamp.getTime();
api.commerce().prices().all().then(function(items) {
return Promise.all(items.map(function(item) {
// do something with item
return;
}));
}).then(function(){
console.log("data processed")
return doc;
});
} else {
console.log("no")
return doc;
}
}).then(function() {
console.log("Done")
}).catch(console.log.bind(console));
}
The output is
yes
Done
data processed
But I am expecting
yes
data processed
Done
I understand this is a promise issue but I am not very experienced with this and could use some advice. Now the return doc also seems to trigger too late and thus the data is not saved.
You forgot to return the inner promise:
return api.commerce().prices().all().then(function(items) {
// ...

Issue with mongoose.save never returning inside of promise

Update !!
I fixed my initial issue with the help of Dacre Denny answer below however when writing tests for my code it turned out that the changes were not being saved before the server responded therefor the company collection in my test database was empty, I fixed this issue with the following code
Companies.find({ company_name: company.company_name }).then(found => {
if (found.length !== 0) {
return res.status(400).json({ error: "Company already exists" });
}
var userForms = company.users;
company.users = [];
const finalCompany = new Companies(company);
console.log(finalCompany);
var userPromises = [];
for (var x = 0; x < userForms.length; x++) {
var user = userForms[x].user;
user.company = finalCompany._id;
userPromises.push(userCreation(user));
}
return Promise.all(userPromises).then(responses => {
for (var x in responses) {
if (!responses[x].errors) {
finalCompany.addUser(responses[x]._id);
} else {
res.status(400).json(responses[x]);
}
}
return finalCompany;
});
})
// I moved the save in here !!!
.then((finalCompany) => {
finalCompany.save().then(()=>{
res.status(200).json({signup:"Successful"});
})
},(err) => {
res.json({error: err});
});
});
Original Issue
I am trying to create a mongoose document to represent a company, this code saves the model in my db however it does not seem to be responding with a status code or reply to postman when I make a request
I've used a debugger to step through the code but I am very rusty on my JS and I am afraid I've gone into deep water with promises thats gone over my head.
router.post('/c_signup', auth.optional, (req, res, next) => {
const { body: { company } } = req;
var error_json = cbc(company);
if( error_json.errors.length > 0 ){
return res.status(422).json(error_json);
}
Companies.find({company_name: company.company_name})
.then((found) => {
if (found.length !== 0) {
return res.status(400).json({error: "Company already exists"});
}
var userForms = company.users;
company.users = [];
const finalCompany = new Companies(company);
var userPromises = [];
for (var x =0; x < userForms.length; x ++) {
var user = userForms[x].user;
user.company = finalCompany._id;
userPromises.push(userCreation(user));
}
Promise.all(userPromises).then((responses) => {
for (var x in responses){
if (!responses[x].errors){
finalCompany.addUser(responses[x]._id);
}
else {
res.status(400).json(responses[x]);
}
}
console.log("h2");
finalCompany.save(function () {
console.log("h3");
return res.status(200);
});
})
});
return res.status(404);
});
This is the output from the debug but the execution is hanging here
h2
h3
There are a few issues here:
First, the save() function is asynchronous. You'll need to account for that by ensuring the promise that save() returns, is returned to the handler that it's is called in.
The same is true with the call to Promise.all() - you'll need to add that promise to the respective promise chain by returning that promise to the enclosing handler (see notes below).
Also, make sure the request handler returns a response either via res.json(), res.send(), etc, or by simply calling res.end(). That signals that the request has completed and should address the "hanging behaviour".
Although your code includes res.json(), there are many cases where it's not guaranteed to be called. In such cases, the hanging behaviour would result. One way to address this would be to add res.end() to the end of your promise chain as shown below:
Companies.find({ company_name: company.company_name }).then(found => {
if (found.length !== 0) {
return res.status(400).json({ error: "Company already exists" });
}
var userForms = company.users;
company.users = [];
const finalCompany = new Companies(company);
var userPromises = [];
for (var x = 0; x < userForms.length; x++) {
var user = userForms[x].user;
user.company = finalCompany._id;
userPromises.push(userCreation(user));
}
/* Add return, ensure that the enclosing then() only resolves
after "all promises" here have completed */
return Promise.all(userPromises).then(responses => {
for (var x in responses) {
if (!responses[x].errors) {
finalCompany.addUser(responses[x]._id);
} else {
res.status(400).json(responses[x]);
}
}
console.log("h2");
/* Add return, ensure that the enclosing then() only resolves
after the asnyc "save" has completed */
return finalCompany.save(function() {
console.log("h3");
return res.status(200);
});
});
})
.then(() => {
res.end();
},(err) => {
console.error("Error:",err);
res.end();
});

Angular - For Loop HTTP Callback/Promise

I am trying to write a loop which performs a number of http requests and adds each response to a list.
However, I don't think I am going about it quite the right way.
I think I am not implementing the required promises correctly. The console log after the for loop shows myList array as empty.
Code:
var _myList = []
function getStuff() {
var deferred = $q.defer()
var url = someUrl
$http.get(url).success(function(response) {
if ( response.array.length > 0 ) {
// loop starts here
for ( var i=0; i < response.array.length; i++ ) {
getThing(response.array[i].id);
};
// check the varibale here
console.log(_myList);
deferred.resolve('Finished');
} else {
deferred.resolve('No stuff exists');
};
}).error(function(error) {
deferred.reject(error);
});
return deferred.promise;
};
function getThing(thindId) {
var deferred = $q.defer()
var url = someUrl + thingId;
$http.get(url).success(function(response) {
_myList.push(response);
deferred.resolve(response);
}).error(function(error) {
deferred.reject(error);
});
return deferred.promise;
};
You can simplify your code as follows:
var allThings = response.array.map(function(id){
var singleThingPromise = getThing(id);
//return a single request promise
return singleThingPromise.then(function(){
//a getThing just ended inspect list
console.log(_myList);
})
});
$q.all(allThings).then(function(){
//only resolve when all things where resolved
deferred.resolve('Finished');
}, function(e){
deferred.reject('Something went wrong ' + e);
});
You indeed won't be able to populate _myList array with for-loop like you set up. Instead create an array of promises - one per data item in response.array and return it as inner promise.
function getStuff() {
var url = someUrl;
return $http.get(url).then(function(response) {
if (response.data.array.length > 0) {
return $q.all(response.data.array.map(function(data) {
return getThing(data.id);
}));
} else {
return 'No stuff exists';
}
});
}
function getThing(thindId) {
var url = someUrl + thingId;
return $http.get(url).then(function(response) {
return response.data;
});
}
After that you would use getStuff like this:
getStuff().then(function(myList) {
console.log(myList);
});

Javascript for loop Promises

I have an array of urls like this
var urls = ["www.google.com", "www.yahoo.com"];
And I want to loop though the urls and perform an async task inside the loop and not move on to the next item until the async task has finished. I know you can do this with promises but I have having some trouble with it. Here what I have
var xmlReader = require('cloud/xmlreader.js');
function readResponse_async(xlmString) {
var promise = new Parse.Promise();
xmlReader.read(xlmString, function (err, res) {
if(err) {
promise.reject(err);
} else {
promise.resolve(res);
}
});
return promise;
}
for (i = 0; i < urls.length; i++) {
Parse.Cloud.httpRequest({
url: unionUrls[i],
}).then(function(httpResponse) {
try {
// console.log(httpResponse.text)
return readResponse_async(httpResponse.text)
} catch (e) {console.log(e)}
}
But right now it doesn't wait for the readResponse_async to finish, how can I have it wait for that?
Thanks
EDIT
After reading the response I make a save to my database and I have another array like this
var location = ['USA', 'England'];
And I make the save like this
function saveLoc_async(data, location) {
var i3, i4, i5, m,
TestItem = Parse.Object.extend("TestItem"),//can be reused within the loops?
promise = Parse.Promise.as();//resolved promise to start a long .then() chain
for (i3 = 0; i3 < data.count(); i3++) {
(function(testItem) {
testItem.set("item", data.at(i));
testItem.set("location", location);
//build the .then() chain
promise = promise.then(function() {
return testItem.save();
});
})(new TestItem());
//************************
//CALL retry(); here?
//**************************
}
Because with your answer I have
function retry() {
if (urlsUnion.length > 0) {
var nextUrl = urlsUnion.pop();
//********** ADDED LINE
var nextLoc = location.pop();
Parse.Cloud.httpRequest({
url: nextUrl,
}).then(function(httpResponse) {
xmlReader.read(httpResponse.text, function (err, res) {
if(err) {
// show an error
} else {
//********** ADDED LINE
saveLoc_async(res, nextLoc);
retry();
}
});
});
}
}
SO where should retry(); go because right now with the save sometimes it puts the second location with one of the first items url? why would that happen?
I did something similar to this for an animation.
var actions = [drawXXX, fadeOutYYY, drawXYZ];
this.startAnimation = function () {
actions.reduce(function (previousAction, nextAction) {
return previousAction.then(nextAction)
}, $.when());
}
Your code fires both urls immediately, and does not wait in-between.
What you would have to do is to remove the first url from the array and fire it. In the 'then' branch check if you still have url's in the array and repeat.
Like this (untested, edited to make the code clean again):
var xmlReader = require('cloud/xmlreader.js');
function readResponse_async(xlmString) {
xmlReader.read(xlmString, function (err, res) {
if(err) {
// show an error
} else {
readFirstUrl();
}
});
}
function readFirstUrl() {
if (urlsUnion.length == 0) {
return;
}
var url = urlsUnion.pop();
Parse.Cloud.httpRequest({
url: url,
}).then(function(httpResponse) {
readResponse_async(httpResponse.text);
});
}
readFirstUrl();
Not sure I understand your use of unionUrls array, but if you have your URL's in a urls array, I think this is pretty clean:
function getUrl(url) {
return Parse.Cloud.httpRequest(url)
.then( function(httpResponse) {
return readResponse_async(httpResponse.text);
});
}
urls.reduce( function(prev, url) {
return prev ? prev.then( function() { getUrl(url); }) : getUrl(url);
}, null);

Combining two promises

I am really new to JavaScript and promises and to be honest I don't fully understand how promises work so I need some help.
I am using Google Cloud Messaging to push notifications from my site to my users. When users receive a notification and clicks on it, it opens a URL stored in a IndexedDB.
importScripts('IndexDBWrapper.js');
var KEY_VALUE_STORE_NAME = 'key-value-store', idb;
function getIdb() {
if (!idb) {
idb = new IndexDBWrapper(KEY_VALUE_STORE_NAME, 1, function (db) {
db.createObjectStore(KEY_VALUE_STORE_NAME);
});
}
return idb;
}
self.addEventListener('notificationclick', function (event) {
console.log('On notification click: ', event);
event.notification.close();
event.waitUntil(getIdb().get(KEY_VALUE_STORE_NAME, event.notification.tag).then(function (url) {
var redirectUrl = '/';
if (url) redirectUrl = url;
return clients.openWindow(redirectUrl);
}));
});
So in the code above, I know that the getIdb()...then() is a promise, but is the event.waitUntil also a promise?
The problem with the above code is that it opens a instance of Chrome every time the notification is clicked and I would prefer that it would utilize an existing instance if available. The following does just that:
self.addEventListener('notificationclick', function(event) {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
event.waitUntil(
clients.matchAll({
type: "window"
})
.then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/' && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
});
However, now I have two promises, getIdb and clients.matchAll and I really have no idea how to combine the two promises and the two sets of code. Any help would be greatly appreciated. Thanks!
For reference, here is IndexDBWrapper.js:
'use strict';
function promisifyRequest(obj) {
return new Promise(function(resolve, reject) {
function onsuccess(event) {
resolve(obj.result);
unlisten();
}
function onerror(event) {
reject(obj.error);
unlisten();
}
function unlisten() {
obj.removeEventListener('complete', onsuccess);
obj.removeEventListener('success', onsuccess);
obj.removeEventListener('error', onerror);
obj.removeEventListener('abort', onerror);
}
obj.addEventListener('complete', onsuccess);
obj.addEventListener('success', onsuccess);
obj.addEventListener('error', onerror);
obj.addEventListener('abort', onerror);
});
}
function IndexDBWrapper(name, version, upgradeCallback) {
var request = indexedDB.open(name, version);
this.ready = promisifyRequest(request);
request.onupgradeneeded = function(event) {
upgradeCallback(request.result, event.oldVersion);
};
}
IndexDBWrapper.supported = 'indexedDB' in self;
var IndexDBWrapperProto = IndexDBWrapper.prototype;
IndexDBWrapperProto.transaction = function(stores, modeOrCallback, callback) {
return this.ready.then(function(db) {
var mode = 'readonly';
if (modeOrCallback.apply) {
callback = modeOrCallback;
}
else if (modeOrCallback) {
mode = modeOrCallback;
}
var tx = db.transaction(stores, mode);
var val = callback(tx, db);
var promise = promisifyRequest(tx);
var readPromise;
if (!val) {
return promise;
}
if (val[0] && 'result' in val[0]) {
readPromise = Promise.all(val.map(promisifyRequest));
}
else {
readPromise = promisifyRequest(val);
}
return promise.then(function() {
return readPromise;
});
});
};
IndexDBWrapperProto.get = function(store, key) {
return this.transaction(store, function(tx) {
return tx.objectStore(store).get(key);
});
};
IndexDBWrapperProto.put = function(store, key, value) {
return this.transaction(store, 'readwrite', function(tx) {
tx.objectStore(store).put(value, key);
});
};
IndexDBWrapperProto.delete = function(store, key) {
return this.transaction(store, 'readwrite', function(tx) {
tx.objectStore(store).delete(key);
});
};
One way to deal with multiple promises is with Promise.all
Promise.all([promise0, promise1, promise2]).then(function(valArray) {
// valArray[0] is result of promise0
// valArray[1] is result of promise1
// valArray[2] is result of promise2
});
read about promise.all - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
event.waitUntil() takes a promise - this allows the browser to keep your worker alive until you've finished what you want to do (i.e. until the promise that you gave to event.waitUntil() has resolved).
As the other answer suggests, you can use Promise.all() within event.waitUntil. Promise.all() takes an array of promises and returns a promise, so you can call then on it. Your handling function will get an array of promise results when all of the promises you've provided to Promise.all have resolved. Your code will then look something like this (I haven't actually tested this, but it should be close):
self.addEventListener('notificationclick', function (event) {
event.notification.close();
event.waitUntil(Promise.all([
getIdb().get(KEY_VALUE_STORE_NAME, event.notification.tag),
clients.matchAll({ type: "window" })
]).then(function (resultArray) {
var url = resultArray[0] || "/";
var clientList = resultArray[1];
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/' && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
return clients.openWindow(url);
}
}));
});
Though I am very late to the party but,
I think you can use this very light, vanilla JS library that does things for you. https://www.npmjs.com/package/easy-promise-all
Here is the small code sample for it.
var { EasyPromiseAll } = require('easy-promise-all');
EasyPromiseAll({
resultFromPromise1: Promise.resolve('first'),
resultFromPromise2: Promise.reject('second'),
}).then((results) => {
const { resultFromPromise1, resultFromPromise2 } = results;
console.log(resultFromPromise1, resultFromPromise2);
});

Categories

Resources