JS Closures, Redis, loop, Async :: empty array

Problem

I give up on this. May some of the wise stackoverflow monks please fix my bugs?

Code is self explaining. Client sends room names, server does a redis lookup and pushes valid rooms to the array. After adding all the rooms, the list should be emitted to the client.

Problem is closure, async etc. based. I understand the problem but cannot get a workaround because the array needs to remain inside the function. Tricky.

Code:

function roomList(socket){

  var roomlist = [], rooms = getRooms(), p = /pChannel_/;

  redis.select(7, function(err,res){

    for (var k in rooms){

      if(rooms[k] != '' && p.test(rooms[k])){

        var key = 'channel:'+rooms[k];

        redis.hgetall(key, function (err, reply) { 

          if(reply){ 
            var c = io.sockets.manager.rooms[rooms[k]];
            roomlist.push( Array(reply['name'],c.length,reply['icon']) );
          }
          else { console.log('nothing found'); }

        });

      }

    }

    // here be dragons
    console.log(roomlist);
    socket.emit('roomList', roomlist);

  });

}

Thanks.

Problem courtesy of: user2429266

Solution

C'mon guys. The OP explicitly said she/he is interested by understanding how things are supposed to work. And you don't need Q or async or any other 3rd party modules to implement this.

In the initial code, there are two problems:

  • with Javascript, the closure scope is at function level, not block level. A function must be introduced to define a proper closure. Here, a simple forEach can be used.

  • the final step (i.e. emit) is not run after the replies have been received from Redis. It must be called in the loop itself. In order to achieve it, it is required to count the items so that the inner callback can test whether the process is complete or not.

So here is another version:

function roomList(socket){

  var roomlist = [], rooms = getRooms(), p = /pChannel_/;

  redis.select(7, function(err,res){
    var count = rooms.length
    rooms.forEach( function(r) {
      if( r != '' && p.test(r) ) {
        var key = 'channel:'+r
        redis.hgetall(key, function (err, reply) { 
          if(reply) { 
            var c = io.sockets.manager.rooms[r];
            roomlist.push( Array(reply['name'],c.length,reply['icon']) );
          } else {
            console.log('nothing found');
          }
          if ( --count <= 0 ) {
            // here be dragons
            console.log(roomlist);
            socket.emit('roomList', roomlist);
          }
        });
      } else --count;
    });
  });
}
Solution courtesy of: Didier Spezia

Discussion

Looks like a job for async.map:

function roomList(socket){
  var rooms = getRooms(), p = /pChannel_/;

  redis.select(7, function(err, res) {
    async.map(rooms, function(room, callback) {
      if (room === '' || ! p.test(room))
        return callback(null, null);

      var key = 'channel:' + room;
      var c   = io.sockets.manager.rooms[room];
      redis.hgetall(key, function (err, reply) {
        if (err)
          callback(err); // propagate Redis errors to final callback, don't know
                         // if you want that or not; use 'callback(null)' if not.
        else
        if (reply)
          callback(err, Array(reply.name, c.length, reply.icon) );
        else
          callback(err, null);
      });
    }, function(err, roomlist) {
      if (err)
         // handle Redis errors...

      // filter 'null' entries from roomlist
      roomlist = roomlist.filter(function(room) { return room !== null });
      console.log(roomlist);
      socket.emit('roomList', roomlist);
    });
  });
}

(untested)

Discussion courtesy of: robertklep

If you just want to wait for the room list to be fully built before emitting the response (as seems highly reasonable), and assuming Q to be available, then you should just need a few additional lines of Q magic plus a closure-forming wrapper around the inner code to maintain a reliable reference to a Q deferred at each pass of the for loop.

function roomList(socket) {
    redis.select(7, function(err, res) {
        var list = [],
            rooms = getRooms(),
            p = /pChannel_/,
            promises = [];
        for(var k in rooms) {
            if(rooms[k] != '' && p.test(rooms[k])) {
                (function(dfrd) {
                    promises.push(dfrd.promise);
                    var key = 'channel:' + rooms[k];
                    redis.hgetall(key, function(err, reply) {
                        if(reply) {
                            var c = io.sockets.manager.rooms[rooms[k]];
                            list.push( [reply['name'], c.length, reply['icon']] );
                        }
                        else {
                            console.log('nothing found');
                        }
                        dfrd.resolve();
                    });
                })(Q.defer());
            }
        }
        Q.all(promises).then(function() {
            console.log(list);
            socket.emit('roomList', list);
        });
    });
}
Discussion courtesy of: Beetroot-Beetroot

This recipe can be found in it's original form on Stack Over Flow.