skip to Main Content

I was following the code snippet here https://github.com/socketio/socket.io-redis#redisadapterclientsroomsarray-fnfunction

io.in('room3').clients((err, clients) => {
  console.log(clients); // an array containing socket ids in 'room3'
});

to get the clients in a particular room.

Is there a simple/idiomatic way I can make this snippet synchronous? I want to loop over an array of rooms and synchronously get the count of users clients.length in each room (ie don’t iterate over the loop until the user count of the current room has been retrieved.)

5

Answers


  1. You can make use of Promises and async await within a for loop

     async function getClients() {
        for(let room in rooms) {
          try{
           const promise = new Promise((res, rej) => {
               io.in(room).clients((err, clients) => {
                  if(err) {
                     rej(err);
                  } else {
                      res(clients); // an array containing socket ids in room
                  }
               });
           })
           const clients = await promise;
           console.log(clients);
          }catch(err) {
            console.log(err)
          }
       };
    }
    

    Using the above manner will help you iterate over each room and get the clients sequentially one by one.

    Although you can’t force them to run synchronously

    Login or Signup to reply.
  2. you can’t force asynchronous things to become synchronous in js. You can execute asynchronous things sequentially though (or in parallel if you wish).

    callbacks:

    function _getTotalClientsCb(rooms, count, cb) {
      if (rooms.length) {
        const room = rooms.shift()
        io.in(room).clients((err, clients) => {
          if (err) 
            return cb(err)
          count += clients.length;
          _getTotalClientsCb(rooms, count, cb)
        })
      } else {
        cb(null, count)
      }
    }
    
    function getTotalClientsCb(rooms, cb) {
      _getTotalClientsCb(rooms.slice(), 0, cb)
      // parallel execution
      // if (!rooms.length)
      //  cb(null, 0)
      // const allClients = [];
      // let count = 0
      // for (let room in rooms) {
      //   io.in(room).clients((err, clients) => {
      //     if (err)
      //       cb(err)
      //     allClients.push(clients)
      //     count += clients.length
      //     if (allClients.length === rooms.length) {
      //       cb(null, count)
      //     }
      //   })
      // }
    }
    
    getTotalClientsCb(rooms, (err, total) => console.log(total))
    

    promises without async / await:

    function clientsPromise(room) {
      return new Promise((resolve, reject) => {
        io.in(room).clients((err, clients) => {
          if (err)
            reject(err)
          resolve(clients)
        })
      })
    }
    
    function getTotalClientsP(rooms) {
      return rooms.reduce((clientP, room) => 
        clientP.then(count => 
          clientsPromise(room).then(clients => 
            count += clients.length
          )
        )
      , Promise.resolve(0));
      // parallel execution
      // return Promise.all(rooms.map(room => clientsPromise(room))).then(
      //   allClients => allClients.reduce((count, clients) => count += clients.length, 0)
      // )
    }
    
    getTotalClientsP(rooms).then(total => console.log(total))
    

    with async / await (builds off answer from @Shubham Katri)

    function getTotalClientsAA(rooms) {
      let count = 0
      return new Promise(async (resolve, reject) => {
        for (let room in rooms) {
          try {
            const clients = await clientsPromise(room);
            count += clients.length
          } catch(err) {
            reject(err)
          }
        };
        resolve(count)
      })
    }
    
    getTotalClientsAA(rooms).then(total => console.log(total))
    

    or you could make use of either promise based method inside your function that needs the count by declaring it async (though this may cause unintended issues in some frameworks):

    async function myMainFucntion() {
       const rooms = ['1', '2', '2'];
       const totalClients = await getTotalClientsP(rooms); // or getTotalClientsAA(rooms)
       console.log(totalClients);
    }
    

    rxjs (external lib but very idiomatic IMO):

    import { bindNodeCallback, concat } from 'rxjs';
    import { reduce } from 'rxjs/operators';
    
    // for parallel
    // import { forkJoin } from 'rxjs'
    // import { map } from 'rxjs/operators';
    
    function clients$(room) {
      return bindNodeCallback(io.in(room).clients)()
    }
    
    function getTotalClients$(rooms) {
      return concat(...rooms.map(room => clients$(room))).pipe(
        reduce((count, clients) => count += clients.length, 0)
      )
      // parallel execution
      // return forkJoin(rooms.map(room => clients$(room))).pipe(
      //   map(allClients => allClients.reduce((count, clients) => count += clients.length, 0))
      // )
    }
    
    getTotalClients$(rooms).subscribe(total => console.log(total))
    

    and a stackblitz to play with these:

    https://stackblitz.com/edit/rxjs-xesqn9?file=index.ts

    Login or Signup to reply.
  3. This works for me

    const EventEmitter = require('events');
    
    const myEmitter = new EventEmitter();
    
    let rooms = ['room1', 'room2', 'room3']
    
    rooms.forEach(room => {
    
        io.in(room).clients((err, clients) => {
          myEmitter.emit('cantUsers', room, clients.length); 
        });
    
      });
    
    myEmitter.on('cantUsers', (room, cant) => {
      console.log(`In ${room} there are ${cant} users online`);
    });
    
    Login or Signup to reply.
  4. I have something simple and functional, though it uses a library I created so it’s not quite idiomatic. There’s no good way to turn async code into sync code, but this way you don’t have to worry about that kind of thing.

    const { pipe, map, reduce, get } = require('rubico')
    
    const rooms = ['room1', 'room2', 'room3']
    
    const getClientsInRoom = room => new Promise((resolve, reject) => {
      io.in(room).clients((err, clients) => {
        if (err) {
          reject(err);
        } else {
          resolve(clients);
        }
      })
    });
    
    const add = (a, b) => a + b
    
    const getTotalClientsCount = pipe([
      map(getClientsInRoom), // [...rooms] => [[...clients], [...clients], ...]
      map(get('length')), // [[...clients], [...clients], ...] => [16, 1, 20, 0, ...]
      reduce(add, 0), // [16, 1, 20, 0, ...] => 0 + 16 + 1 + 20 + 0 + ...
    ]);
    

    then you would use the function getTotalClientsCount on your array of rooms like so

    async function main() {
      const rooms = ['room1', 'room2', 'room3']
      const totalCount = await getTotalClientsCount(rooms)
      console.log(totalCount)
    };
    main();
    

    if you really wanted to get fancy, you could use a transducer to get the total count of clients in rooms without creating any intermediate arrays

    const getTotalClientsCountWithTransducer = reduce(
      pipe([
        map(getClientsInRoom), // room => [...clients]
        map(get('length')), // [...clients] => 16
      ])(add), // 0 + 16 + ... ; add client count from room to total count, repeat for next room
      0,
    );
    async function main() {
      const rooms = ['room1', 'room2', 'room3']
      const totalCount = await getTotalClientsCountWithTransducer(rooms)
      console.log(totalCount)
    };
    main();
    

    I write a crash course on transducers here

    Login or Signup to reply.
  5. The other answers here seem to overcomplicate things. This can easily be done without any 3rd party libraries.

    Wrapping a callback in a Promise gives you more control over the flow of the program.

    // The purpose of this is just to convert a callback to a Promise:
    // clientsInRoom('room1') is a promise that resolves to `clients` for that room.
    const clientsInRoom = room => new Promise((resolve, reject) => {
      io.in(room).clients((err, clients) => 
        err ? reject(err) : resolve(clients)
      )
    })
    

    If you are inside of an async function, you can use await, which will make asynchronous code feel more like synchronous code. (although “top-level await” is supported within modules in modern browsers)

    async function main() {
      const rooms = ['room1', 'room2', 'room3']
    
      // If either one of the promises reject, the statement will reject
      // Alternatively, you can use Promise.allSettled()
      const allClients = await Promise.all(rooms.map(clientsInRoom))
    
      // You can use map to turn this into an array of the lengths:
      const lengths = allClients.map(clients => clients.length)
    
      // Alternatively, if you want the feel of a synchronous loop:
      for (const clients of allClients) {
        console.log(clients.length)
      }
    }
    

    Using for-await is also an option, if you want to start iterating before all promises have resolved:

    async function main() {
      const rooms = ['room1', 'room2', 'room3']
      for await (const clients of rooms.map(clientsInRoom)) {
        console.log(clients.length)
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search