简体   繁体   中英

How to make socket.io asynchronous method synchronous?

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.)

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

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)
  }
}

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

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

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`);
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM