1

I'm working with node.js and redis. I've got a redis database with a bunch of keys. Something like this:

user/chris/potion user/pete/potion user/chris/race user/pete/race user/chris/weapon user/pete/weapon

I want to do a redis call which retrieves all user stats, puts the stats into a JS object, then passes it to the client for displaying character stats in the browser. Using javascript I inject the username chris at u into the redis call like this:

KEYS user/u/*

which returns:

1) "user/chris/weapon"
2) "user/chris/race"
3) "user/chris/potion"

Now I can iterate through those results, get the value of each key with GET, and make a javascript object. Seems super simple so I write the code. I quickly run into problems using forEach:

var redis = require('redis');
var client = redis.createClient();

    exports.getUserObject = function(requesteduser, callback) {

        var userstats = {}; // the object to hold the user stats once retrieved from the db
        client.KEYS('user/' + requesteduser + '/*', function(err, replies) {

             replies.forEach(function (reply, i) {
                 client.GET(reply, function(err, value) {
                      // get the key name so we can populate a js object
                      var n = reply.lastIndexOf('/');
                      var key = reply.substring(n + 1, reply.length);
                      userstats[key] = value;

                      console.dir(userstats);

                      callback(null, userstats); // parent expects (err, userstats)
                 });

             });
        });
}

When ran, output is like this:

{ weapon: 'twosword' }
{ weapon: 'twosword', race: 'elf' }
{ weapon: 'twosword', race: 'elf', potion: 'mana' }

callback(null, userstats) is called three times. Calling callback more than once will be problematic, since eventually callback will trigger data being sent data to the client.

I think I know what is happening. client.GET is ran three times because I asked it to. The replies array has three elements, so each time the result comes in for client.GET, the callback is called.

What I need to happen is for the forEach loop to finish iterating through all the redis keys, and once that's done, call the callback once. I tried solving this problem first with promises, then with async, because async has async.each. I got stuck solving the problem with both. I'm back to square one now, I'm convinced I have to do something different with forEach to make progress.

How can I accomplish this?

3
  • Would moving callback(null, userstats); outside of the forEach not solve the problem? Commented Mar 22, 2014 at 2:58
  • @Greg, That was one of the first things I tried but because client.GET(?) is an asynchronous function,the callback gets called before client.GET returns the first value. I end up with an empty userstats object. Commented Mar 22, 2014 at 3:05
  • of course! forgot the async ;) Commented Mar 22, 2014 at 3:13

2 Answers 2

2

Since you're iterating over replies, you can check when you've reached the last element and only call callback in that instance.

client.KEYS('user/' + requesteduser + '/*', function(err, replies) {

         replies.forEach(function (reply, i) {
             client.GET(reply, function(err, value) {
                  // get the key name so we can populate a js object
                  var n = reply.lastIndexOf('/');
                  var key = reply.substring(n + 1, reply.length);
                  userstats[key] = value;

                  console.dir(userstats);

                  if (i == replies.length-1) callback(null, userstats); // parent expects (err, userstats)
             });

         });
    });
Sign up to request clarification or add additional context in comments.

Comments

2

I know it's a little late to help @Grimtech but I'll leave my opinion for other people that arrives here eventually:

First of all I rather to model @Grimtech's problem differently. Something like this:

client.hmset("user:chris", "weapon", "some weapon", "race", "some race", "potion", "some potion");
client.hmset("user:pete", "weapon", "same weapon", "race", "other race", "potion", "other potion");

Then I would have a method in Node returning a json like the following, assuming that I can have more than one key starting with "user:chris":

router.get('/users/:user_id', function(req, res, next) {
    var response = [];
    var client = redis.createClient();
    client.keys("user:" + req.params.user_id + "*", function (err, users) {
        users.forEach(function (key, pos) {
            client.hgetall(key, function (err, user) {
                response.push(user);
                if (pos === users.length - 1) {
                    res.json(response);
                    client.quit();
                }
            });
        });
    });
});

The if (pos === users.length - 1) solves the async issue.

The json returned will have all the "user:chris" attributes so yo can do whatever you want in the client browser.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.