SocketCluster. The most underrated framework. Part 3: A Pub/Sub example and middleware
Tags: socketcluster, opensource, node, javascript
Published: 2021-04-15 16:41:22

{% github maarteNNNN/sc-underrated-framework-pubsub %}

## Introduction

In this part we will make a simple chat example to understand how Pub/Sub works in SocketCluster. The app can be tested across multiple browser windows. We will add some simple middlewares. A chat history and censorship for bad-words.

## Setup

Let's setup a blank project by running `socketcluster create sc-pubsub` and `cd sc-pubsub`. Let's install nodemon to restart the server automatically `npm i -D nodemon`. And for our bad-words censorship we will use a package called [bad-words](https://www.npmjs.com/package/bad-words) from NPM. `npm i -s bad-words`. The server can be run with `npm run start:watch`.

### Client code setup (don't give much attention to this, just copy and paste)

We will use vanilla JavaScript in HTML like [part 2](https://dev.to/maartennnn/socketcluster-the-most-underrated-framework-part-2-setting-up-5b3o) shipped with SocketCluster in `public/index.html`. Let's delete everything inside the `style` tag and replace it with:

```css
* {
  margin: 0;
  padding: 0;
}

html {
  height: 100vh;
  width: 100vw;
}

.container {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.chat-history {
  height: 70vh;
  width: 75%;
  border: 1px solid #000;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.chat-input {
  width: 75%;
  height: 5vh;
  border-left: 1px solid #000;
  border-bottom: 1px solid #000;
  border-right: 1px solid #000;
}

input {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  border: none;
  padding: 0 1em;
}

strong,
small {
  font-size: 11px;
  color: gray;
}

.message {
  padding: 0.25rem 1rem;
}
```

and delete eveything inside `<div class="container">` tag and replace it with:

```html
<div id="chat-history" class="chat-history"></div>
<div class="chat-input">
  <input placeholder="message" onkeyup="sendMessage(event)" />
</div>
```

Okay. Now we have a basic chat page. Nothing too fancy. Now we can focus on getting actual logic of our chat application.

## The Pub/Sub functionality

### Client

Pub/Sub in SocketCluster is something that can work without writing any backend logic. We can create a channel on the client and the server makes this channel available for other clients.

```js
(async () => {
  for await (const data of socket.subscribe('chat')) {
    console.log(data);
  }
})();
```

and we should create the function that listens to the `enter` key on the input to send the publish the message.

```js
const sendMessage = async (event) => {
  if (event.keyCode === 13) {
    try {
      await socket.transmitPublish('chat', {
        timestamp: Date.now(),
        message: event.target.value,
        socketId: socket.id,
      });
      event.target.value = '';
    } catch (e) {
      console.error(e);
    }
  }
};
```

The `transmitPublish` method does not suspect a return value. If you do want a response you can look at [`invokePublish`](https://socketcluster.io/docs/api-ag-client-socket/#methods).

The `transmitPublish` sends an object with a `timestamp`, `message` and the `socketId`. The `socket.subscribe('chat')` [async iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) will log any new data being pushed. Open two browser windows next to each other and open the Developer Tools in both windows. If you send a message in one window it should output it in both consoles.

We will display the messages in the `#chat-history` `div` by creating a function that creates an element, changes the text, adds a class and appends the element.

```js
const createMessage = ({ socketId, timestamp, message }) => {
  const chatHistoryElement = document.getElementById('chat-history');
  const messageElement = document.createElement('div');
  messageElement.className = 'message';
  messageElement.innerHTML = `<strong>${socketId}</strong> <small>${timestamp}:</small> ${message}`;

  chatHistoryElement.appendChild(messageElement);

  // Always scroll to the bottom
  chatHistoryElement.scrollTop = chatHistoryElement.scrollHeight
};
```

change the previous `console.log(data)` inside the `socket.subscribe('chat')` to `createMessage(data)`.

Now if we send messages it should display them in the HTML instead of the Developer Tools. Pretty neat, huh? Upon to this point we still didn't do any server-side code.

### Server-side

There's only one problem with our app. Every new window does not have any older messages. This is where the server comes in. We will create a middleware that pushes every message to an array, for simplicity sake. Another thing the middleware will pick-up is bad-words. We can filter them and replace the characters with a `*`.

```js
const Filter = require('bad-words');
const filter = new Filter();

...

const history = []

agServer.setMiddleware(
  agServer.MIDDLEWARE_INBOUND,
  async (middlewareStream) => {
    for await (const action of middlewareStream) {
      if (action.type === action.PUBLISH_IN) {
        try {
          // Censor the message
          action.data.message = filter.clean(action.data.message);
        } catch (e) {
          console.error(e.message);
        }
        // Push to the array for history
        history.push(action.data);
      }
      // Allow the action
      action.allow();
    }
  },
);

...
```

We set an inbound middleware, we pass it an async iterable stream. On every `action` of the stream we check if the `action.type` equals the constant provided by SC `action.PUBLISH_IN`. If the conditional is true we filter the message and allow the action. Alternatively we could `action.block()` the action if we don't want it to go through. [More on middleware here](https://socketcluster.io/docs/middleware-and-authorization/)

To implement the history it's pretty simple, we just create a constant `const history = []` and push every `action.data` to it. Like shown in the above code.

To initially get the history we `transmit` the data upon a socket connection (e.g. a new browser window).

```js
(async () => {
  for await (let { socket } of agServer.listener('connection')) {
    await socket.transmit('history', history);
  }
})();
```

And create a receiver on the client which uses a loop to create the messages.

```js
(async () => {
  for await (let data of socket.receiver('history')) {
    for (let i = 0; i < data.length; i++) {
      const m = data[i];
      createMessage(m);
    }
  }
})();
```

I will try to add an article every two weeks.