express/typescript: on the server side, how to create a global variable to store multiple sockets?

I’m using Express + Typescript for my webpage’s backend.

I have defined web sockets with socket.io:

/src/index.ts:

const initializeExpress = (): void => {
  const app = express();

  let http = require("http").Server(app);
  let io = require("socket.io")(http, {
    cors: {}
  });
  
  const socket_ids = {};
  
  io.on("connection", function(socket: any) {
    console.log("a user connected. socket.id: ", socket.id, "socket.username: ", socket.username);    
    socket_ids[socket.username] = socket.id;
  });

  const server = http.listen(3001, function() {
    console.log("listening on *:3001");
  });


  // register middleware
  io.use((socket, next) => {
    socket.username = socket.handshake.auth.username;
    socket.organization = socket.handshake.auth.organization;
    next();
  });

  // sends each client something constantly
  setInterval(() => {
    for (const u in socket_ids){
      const my_socket = io.sockets.sockets.get(socket_ids[u]);
      my_socket.emit("knock", "knock-knock");
    }
  }, 10000);

  app.use(cors());
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(bodyParser.json());
  app.use(express.static(path.join(__dirname, 'build')));
  
  app.use(addRespondToResponse);

  attachPublicRoutes(app);
};

initializeExpress();

when the backend is started, I can confirm that the socket is working because I can receive the knock-knock messages that my code emits to every client’s socket.

Now, in many other files, I need to do socket.emit() to specific clients at various places depending on the business logic. So I need to do my_socket = io.sockets.sockets.get(socket_ids[u]).

How to maintain the socket_ids object and the io object globally / access it globally?

Answer

You could implement a global state manager (which basically is just a simple javascript object with get and set methods) defined in a class which can be imported and accessed from any script. Then you attach those variables to that state.

Here’s a very simple, but working version of what that could look like:

export class StateManager {

  static addToState = (key, value) => {
    if (!StateManager._singleton) {
      StateManager._singleton = new StateManager();
    }
    StateManager._singleton.addToState(key, value);
  };

  static readFromState = (key: string) => {
    if (!StateManager._singleton) {
      StateManager._singleton = new StateManager();
    }
    return StateManager._singleton.readFromState(key);
  };

  private static _singleton: StateManager;
  private _state: Record<string, any>;

  private constructor() {
    this._state = {};
  }

  private addToState = (key, value) => (this._state[key] = value);

  private readFromState = (key) => this._state[key];
}

For usage, you would then add this to your code:

import { StateManager } from './state';

// your code where socket_id and io are set

StateManager.addToState('socket_ids', socket_id);
StateManager.addToState('io', io);

// your code where you want to retrieve the values:

const socket_ids = StateManager.readFromState('socket_ids');

See working example on Stackblitz.