Building Scalable WebSocket APIs with Socket.IO Syntax

Building Scalable WebSocket APIs with Socket.IO Syntax

Sockets can only transmit binary (text.)

Imagine we have this data

const message = {id: '123', name: 'Reilly', message: 'Hey'}

Here's the problem: we want to send this data to the ui but we have hundreds of useEffects spread throughout the codebase. How can we make sure that this message is processed in the right part of the codebase and not somewhere else?

The best solution I've seen is to use a unique identifier - like a title.

This is what Socket.IO does.

Here's how we could implement their API from scratch:

The client

function ws(url: string) {
  const socket = new WebSocket(url);

  // This looks scary
  // It's just a way to manage subscriptions
  // The key is the thing we are subbing to 
  // the value is an array of functions to call when we get a message
  const subscriptions: { [key: string]: ((content: any) => void)[] } = {};

  // Connection opened
  socket.addEventListener('open', (event) => {
    socket.send('Hello Server!');
  });

  // Listen for messages
  socket.addEventListener('message', (event) => {
    try {
      const message = JSON.parse(event.data);
      if (!message.type || !message.content) return;
      const { type, content } = message;

      if (!subscriptions[message.type]) return;

      if (subscriptions[type]) {
        subscriptions[type].forEach((fn) => fn(content));
      }
    } catch (error) {
      console.error('Error parsing message', error);
    }
  });

  return {
    on: (type: string, fn: (content: any) => void) => {
      if (!subscriptions[type]) {
        subscriptions[type] = [];
      }
      subscriptions[type].push(fn);
    },
  };
}

Here's how it works:

1. We start with the return part - we know we want the person to say socket.on(...) so we need to return the on property.
2. They provide a string to "sub" them to the message they care about. They give us a function and we call that function with their data when that message comes in.

Now we just need a mechanism to keep track of the messages they've subbed to.

3. We implement an object where the keys are the names of the messages and the values are an array of the functions they provide.
4. Call the functions when the messages come in.

And here's how we can call it:

const socket = ws('ws://localhost:3000/ws')

socket.on('test', (data) => {
   console.log(data);
})

Now your turn! How would you implement the server part so that we could say

socket.emit('message', {id: 0, name: "Reilly", message: 'hi'})

Thanks for reading <3