Pass parameter to callback function inside a forEach loop? - javascript

I am working in client-side JavaScript. I want to run a callback inside a forEach loop, and pass the value of the variable in the forEach loop to the callback.
Here is my (broken) code:
var tags = ['foo', 'bar'];
var displayTag = function(error, result) {
if (error) {
console.warn("Error from server", error);
}
else {
console.log(tag);
}
};
tags.forEach(function(tag) {
if (is_project) {
Project.addTag(visualisation_id, tag, displayTag);
} else {
Page.addTag(page_id, tag, displayTag);
}
});
When I run this I'm getting a warning that tag in console.log(tag) is undefined.
How can I pass the value of tag to the callback? I'm sure this is simple!

Perhaps you can use a Higher-Order Function (a function that returns a function):
var tags = ['foo', 'bar'];
var createDisplayTagCallback = function(tag) {
return function (error, result) {
if (error) {
console.warn("Error from server", error);
}
else {
console.log(tag);
}
}
};
tags.forEach(function (tag) {
if (is_project) {
Project.addTag(visualisation_id, tag, createDisplayTagCallback(tag));
} else {
Page.addTag(page_id, tag, createDisplayTagCallback(tag));
}
}); </script>
Haven't tested this code though...

You must have a callback in addTag
this.addTag(visualisation_id, tag, callback) {
callback(error, result)
}
Change it to
this.addTag(visualisation_id, tag, callback) {
callback(error, result, tag)
}

Related

Accessing this object of a callback function from outside

It might be a bit confusing what I'm asking but I'll try to be as clear as I can.
Basically I'm doing unit test with mocha/chai for my Data Access Layer of my Node.JS server. I'm using bluebird to return a promise and an SQLite Databases.
That's my function insert I want to test :
insert(sqlRequest, sqlParams, sqlRequest2) {
return new Promise(function (resolve, reject) {
let insertStatement = this.getDatabase().prepare(sqlRequest);
let getStatement = this.getDatabase().prepare(sqlRequest2);
insertStatement.run(sqlParams, err => {
console.log('this.changes = ', this.changes);
if (this.changes === 1) {
getStatement.all({ $id: this.lastID }, (err, rows) => {
if (err) {
console.log('entered second err');
reject(err);
} else {
resolve(rows[0]);
}
});
} else {
console.log('entered first err ');
reject(err);
}
});
}.bind(this));
}
And that's my test with mocha :
it('insert : Error 2st SQL query', function (done) {
const daoCommon = new DaoCommon();
daoCommon.getDatabase = () => {
return {
prepare: (sqlRequest) => {
return {
all: (sql, callback) => {
let err = {};
let rows = null;
callback(err, rows);
},
run: (sqlParams, callback) => {
let err = undefined;
callback(err);
}
}
}
}
}
daoCommon.insert('', '', '')
.then(success => {
expect.fail();
})
.catch(error => {
expect(error).to.eql({});
})
.finally(function () {
done();
})
});
I want to simulate a test where the this.changes is equal to 1 but I don't know how/where I can set this value. According to what I've read this this object is from the callback function, but I have no idea exactly from where it comes or how to set it for my tests.
Update:
You can set the this of a function you are calling with .call of the method.
In your case calling callback with this.changes value will look like:
var thisObject = {
changes: 1
};
callback.call(thisObject, err);
This will set the value this.changes of your callback function.
The value of this is explained in the API documentation
If execution was successful, the this object will contain two
properties named lastID and changes which contain the value of the
last inserted row ID and the number of rows affected by this query
respectively.
It means that the callback function will always have this.changes. You can not change it unless you set this.changes = something manually, which I don't understand why would you do that.
Thank for #Maxali comment, I will post the answer below :
You can set this when calling the function callback(err) by using .call(). eg: callback.call({changes:1}, err). this will set changes to 1
And note that I had to change this line insertStatement.run(sqlParams, err => { where I have the callback from an arrow function to a function declaration insertStatement.run(sqlParams, function(err) { for this to work. I assume this is due to the this which in an arrow function doesn't refer to the object inside the function itself.

async.js return value form last function

I would like create find method in my User object. This function should returns user. But for this example it returns only the text.
But I don't know how return value for waterfall.
When I run
console.log(User.find("575578a9f95d6de1354327ef"));
I got 'undefined' in my output, but I except 'function find shoud return this value', What I should to do if I want get 'function find shoud return this value' text on my output
User = {
collectionName: 'users',
find: function(id){
async.waterfall(
[
function(callback) {
MongoClient.connect('mongodb://127.0.0.1:27017/lingogo', function(err,db) {
if(err) { throw err}
callback(null, db, id);
});
},
function(db,id, callback) {
var collection = db.collection(User.collectionName);
collection.find({'_id': ObjectID(id)}).toArray(function (err, result) {
if (err) { throw err };
if (result[0] && result[0]._id != '') {
return callback(null,result[0]);
}
return callback(null,null);
})
},
],
function (err, user) {
return 'function find shoud return this value';
}
);
}
}
console.log(User.find("575578a9f95d6de1354327ef"));
Function find must have a callback too, that you call in a callback of waterfall. You cannot return a value synchronously from an asynchronous function.
find: function (id, callback) {
async.waterfall(..., function (...) {
callback(null, return_value);
});
}
That should be called like
User.find("575578a9f95d6de1354327ef", function (err, return_value) {
console.log(return_value);
});

Callback function getting called before initial function finishes

I am having issues with callback functions - I have a main function which has two callback functions inside. This is the main function
socket.on('play next video', function(data) {
removeVideo(cue[0], getCueFromDb(function() {
io.sockets.emit('next video');
}));
});
My removeVideo function looks like this:
function removeVideo(id, callback) {
Video.find({'id' : id}).remove(function(err, data) {
if (err)
console.log(err)
console.log("Removed video", id)
});
if (callback)
callback();
else
return
}
and the getCueFromDb function looks like this
function getCueFromDb(callback) {
Video.find({}).exec(function(err, videos) {
if (err) {
console.log(err)
}
if (videos.length) {
cue.length = 0 // empty array
videos.forEach(function(video) {
cue.push(video.id) // push all the videos from db into cue array
});
io.sockets.emit('send cue', {cue: cue});
}
else {
console.log("No more videos in database!")
}
if (callback)
callback();
else
return
});
}
However the functions aren't getting called in the correct order - am I doing something wrong?
Your callback needs to be inside your find.remove()
function removeVideo(id, callback) {
Video.find({'id' : id}).remove(function(err, data) {
if (err)
console.log(err)
console.log("Removed video", id)
if (callback)
callback();
else
return
});
You have to change removeVideo so callback we be called only after delete.
That's the code:
function removeVideo(id, callback) {
Video.find({'id' : id}).remove(function(err, data) {
if (err)
console.log(err)
console.log("Removed video", id)
if (callback)
callback();
else
return
}
});
So callback will be called only after vide is really removed.

nodejs async each function

In node js, i am using async function to get the result from another function and store it as array and return the result. But here i am getting empty json as output {}. See the comments inside code block. May i know where i am doing mistake ?
collectSearchResult: function (searchConfig, callback) {
var searchResult = {};
async.each(searchConfig.scope, function (scope, callback) {
var query = {
"query": {
"match": {
"_all": searchConfig.q
}
},
operationPath = scope.url;
this.doSearch(operationPath, query, function (err, results) {
var type = scope.type;
searchResult[type] = results;
// Here i am able to get correct output async
console.log(searchResult);
});
callback();
}.bind(this), function (err) {
// Here it is just returning empty json like {}. this function is called before this.doSearch complete its task
console.log(searchResult);
callback(err, searchResult);
});
}
collectSearchResult: function (searchConfig, callback) {
var searchResult = {};
async.each(searchConfig.scope, function (scope, callback) {
var query = {
"query": {
"match": {
"_all": searchConfig.q
}
},
operationPath = scope.url;
this.doSearch(operationPath, query, function (err, results) {
var type = scope.type;
searchResult[type] = results;
// Here i am able to get correct output async
console.log(searchResult);
//<><><><><><><>
callback(); //you need to place the callback for asynch.each
//within the callback chain of your query, else async.each
//immediately finishes before your data has arrived.
//<><><><><><><>
});
}.bind(this), function (err) {
// Here it is just returning empty json like {}. this function is called before this.doSearch complete its task
console.log(searchResult);
callback(err, searchResult);
});
}

Retry an async function if the error is retryable

How can I change my logic to retry if the err.retryable = true in the following code:
async.each(queues, function (queue, callback) {
sqs.getQueueUrl({'QueueName': queue.queue}, function (err, qurl) {
if (err) {
if (err.retryable) {
// How to retry sqs.getQueueUrl({'QueueName': queue.queue}...?
} else {
console.error(err, err.stack);
callback(err);
}
}else{
//Do lots of things here
}
})
}, function (err) {
//...
})
In addition to the advice by dfsq to name your callback and use it an asynchronously recursive manner, see also async.retry from the async module by Caolan McMahon. Example:
async.retry(3, apiMethod, function(err, result) {
// do something with the result
});
More complex example:
async.auto(
{
users: api.getUsers.bind(api),
payments: async.retry(3, api.getPayments.bind(api))
}, function(err, results) {
// do something with the results
}
);
More details in the docs.
UPDATE
A better solution for your use case:
I wrote a utility function that you can use to make your original method support any number of retries (with err.retryable support).
You can use it this way:
var retryingFunction = withRetries(sqs, sqs.getQueueUrl);
(Note that you need to provide both sqs and sqs.getQueueUrl)
And now you can use the retryingFunction just like you would use sqs.getQueueUrl but with a number of retries as the first arguments. The retries will only be done when err.retryable is true.
So now, instead of:
sqs.getQueueUrl({'QueueName': queue.queue}, function (err, qurl) {
// ...
});
you can use:
retryingFunction(3, {'QueueName': queue.queue}, function (err, qurl) {
// ...
});
where 3 is the number of retries.
And this is the function that I wrote to make the above possible:
function withRetries(obj, method) {
if (!method) {
method = obj;
obj = null;
}
if (typeof method != "function") {
throw "Bad arguments to function withRetries";
}
var retFunc = function() {
var args = Array.prototype.slice.call(arguments);
var retries = args.shift();
var callback = args.pop();
if (typeof retries != "number" || typeof callback != "function") {
throw "Bad arguments to function returned by withRetries";
}
var retryCallback = function (err, result) {
if (err && err.retryable && retries > 0) {
retries--;
method.apply(obj, args);
} else {
callback(err, result);
}
};
args.push(retryCallback);
method.apply(obj, args);
};
return retFunc;
}
See this LIVE DEMO to play with it and see how it works.
It works fine in the demo, I hope it will also work for your code.
You can give queue callback a name and provide it in retry request again. Try this:
async.each(queues, function (queue, callback) {
sqs.getQueueUrl({'QueueName': queue.queue}, function queueCallback(err, qurl) {
if (err) {
if (err.retryable) {
sqs.getQueueUrl({'QueueName': queue.queue}, queueCallback);
} else {
console.error(err, err.stack);
callback(err);
}
} else {
//Do lots of things here
}
});
}, function (err) {
//...
});

Categories