Search code examples
javascriptnode.jsserver-sent-events

Using server-sent events : how to store a connection to a client?


I there,

I have setup a SSE connection between a react-native app and a NodeJS server. I followed some guidelines to set it up in client side, polyfilling EventSource and all, this is pretty straight forward. But on the server side, I have some issue finding out how to store the connection with the client. I chose store the Response in a global object, but I have the feeling this is not the proper way to do. Can somebody advise ?

Here is my code below


const SSE_RESPONSE_HEADER = {
  'Connection': 'keep-alive',
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'X-Accel-Buffering': 'no'
};

const getUserId = (req, from) => {
  try {
    // console.log(from, req.body, req.params)
    if (!req) return null;
    if (Boolean(req.body) && req.body.userId) return req.body.userId;
    if (Boolean(req.params) && req.params.userId) return req.params.userId;
    return null
  } catch (e) {
    console.log('getUserId error', e)
    return null;
  }
}

global.usersStreams = {}

exports.setupStream = (req, res, next) => {

  let userId = getUserId(req);
  if (!userId) {
    next({ message: 'stream.no-user' })
    return;
  }

  // Stores this connection
  global.usersStreams[userId] = {
    res,
    lastInteraction: null,
  }

  // Writes response header.
  res.writeHead(200, SSE_RESPONSE_HEADER);

  // Note: Heatbeat for avoidance of client's request timeout of first time (30 sec)
  const heartbeat = {type: 'heartbeat'}
  res.write(`data: ${JSON.stringify(heartbeat)}\n\n`);
  global.usersStreams[userId].lastInteraction = Date.now()

  // Interval loop
  const maxInterval = 55000;
  const interval = 3000;
  let intervalId = setInterval(function() {
    if (!global.usersStreams[userId]) return;
    if (Date.now() - global.usersStreams[userId].lastInteraction < maxInterval) return;
    res.write(`data: ${JSON.stringify(heartbeat)}\n\n`);
    global.usersStreams[userId].lastInteraction = Date.now()
  }, interval);


  req.on("close", function() {
    let userId = getUserId(req, 'setupStream on close');
    // Breaks the interval loop on client disconnected
    clearInterval(intervalId);
    // Remove from connections
    delete global.usersStreams[userId];
  });

  req.on("end", function() {
    let userId = getUserId(req, 'setupStream on end');
    clearInterval(intervalId);
    delete global.usersStreams[userId];
  });

};

exports.sendStream = async (userId, data) => {
  if (!userId) return;
  if (!global.usersStreams[userId]) return;
  if (!data) return;

  const { res } = global.usersStreams[userId];

  res.write(`data: ${JSON.stringify({ type: 'event', data })}\n\n`);
  global.usersStreams[userId].lastInteraction = Date.now();

};


Solution

  • My first tip is to simply get rid of global; it's ok to have a variable in your module's closure. Your module can encapsulate this "global" state without making it globally accessible to all other modules.

    const usersStreams = {};
    

    Second, it's probably not impossible for the same user to have multiple connections established. I'd recommend that if you're keying these connections on userId, you ought to have the values in userStreams for these keys as collections so you can write to multiple. Either that or you'd need a more unique key.