fbpx

Blogs from the Ranch

< Back to Our Blog

Live Updates With Queues, WebSockets, and Push Notifications. Part 3: WebSockets

In parts one and two of this series, we set up a frontend and backend to view notifications from third-party services like GitHub, Netlify, and Heroku. It works like this:

diagram of HTTP endpoints sending data to a queue, which a worker reads from

Now our client is set up to view our messages, but we need to quit and restart the app to get any updates. We could add pull-to-refresh functionality, but it’d be much nicer if we could automatically receive updates from the server when a new message is received. Let’s build out WebSockets functionality to accomplish these live updates. Here’s an illustration of how the flow of data will work:

diagram of worker pushing data via a WebSocket

If you like, you can download the completed server project and the completed client project for part 3.

Adding WebSockets to the Server

There are a few different libraries that can provide WebSocket functionality to Node apps. For the sake of this tutorial, we’ll use websocket:

$ yarn add websocket

In our worker, after we handle a message on the incoming queue and save the message to the database, we’ll send a message out on another queue indicating that we should deliver that message over the WebSocket. We’ll call that new queue socket. Make the following change in workers/index.js:

const handleIncoming = message =>
  repo
    .create(message)
    .then(record => {
      console.log('Saved ' + JSON.stringify(record));
+     return queue.send('socket', record);
    });

 queue
   .receive('incoming', handleIncoming)

Note the following sequence:

  • We receive a message on the incoming queue;
  • Then, we save the record to the database;
  • And finally, we send another message out on the socket queue.

Note that we haven’t yet implemented the WebSocket code to send the response to the client yet; we’ll do that next. So far, we’ve just sent a message to a new queue that the WebSocket code will watch.

Now let’s implement the WebSocket code. In the web folder, create a file socket.js and add the following:

const WebSocketServer = require('websocket').server;

const configureWebSockets = httpServer => {
  const wsServer = new WebSocketServer({ httpServer });
};

module.exports = configureWebSockets;

We create a function configureWebSockets that allows us to pass in a Node httpServer and creates a WebSocketServer from it.

Next, let’s add some boilerplate code to allow a client to establish a WebSocket connection:

 const configureWebSockets = httpServer => {
   const wsServer = new WebSocketServer({ httpServer });
+
+  let connection;
+
+  wsServer.on('request', function(request) {
+    connection = request.accept(null, request.origin);
+    console.log('accepted connection');
+
+    connection.on('close', function() {
+      console.log('closing connection');
+      connection = null;
+    });
+  });
 };

All we do is save the connection in a variable and add a little logging to indicate when we’ve connected and disconnected. Note that our server is only allowing one connection; if a new one comes in, it’ll be overwritten. In a production application you would want to structure your code to handle multiple connections. Some WebSocket libraries will handle multiple connections for you.

Next, we want to listen on the socket queue we set up before, and send an outgoing message on our WebSocket connection when we get one:

 const WebSocketServer = require('websocket').server;
+const queue = require('../lib/queue');

 const configureWebSockets = httpServer => {
...
   wsServer.on('request', function(request) {
...
   });
+
+  queue
+    .receive('socket', message => {
+      if (!connection) {
+        console.log('no WebSocket connection');
+        return;
+      }
+      connection.sendUTF(JSON.stringify(message));
+    })
+    .catch(console.error);
 }

When a message is sent on the socket queue and if there is no WebSocket client connection, we do nothing. If there is a WebSocket client connection we send the message we receive out over it.

Now, we just need to call our configureWebSockets function, passing our HTTP server to it. Open web/index.js and add the following:

 const listRouter = require('./list');
+const configureWebSockets = require('./socket');

 const app = express();
...
 const server = http.createServer(app);
+configureWebSockets(server);

By calling our function, which in turn calls new WebSocketServer(), we enable our server to accept requests for WebSocket connections.

Adding WebSockets to the Client

Now we need to update our Expo client to make that WebSocket connection to the backend and accept messages it sends, updating the screen in the process. On the frontend we don’t need to add a dependency to handle WebSockets; the WebSocket API is built-in to React Native’s JavaScript runtime.

Open src/MessageList.js and add the following:

 const httpUrl = Platform.select({
   ios: 'http://localhost:3000',
   android: 'http://10.0.2.2:3000',
 });
+const wsUrl = Platform.select({
+  ios: 'ws://localhost:3000',
+  android: 'ws://10.0.2.2:3000',
+});
+
+let socket;
+
+const setUpWebSocket = addMessage => {
+  if (!socket) {
+    socket = new WebSocket(wsUrl);
+    console.log('Attempting Connection...');
+
+    socket.onopen = () => {
+      console.log('Successfully Connected');
+    };
+
+    socket.onclose = event => {
+      console.log('Socket Closed Connection: ', event);
+      socket = null;
+    };
+
+    socket.onerror = error => {
+      console.log('Socket Error: ', error);
+    };
+  }
+
+  socket.onmessage = event => {
+    addMessage(JSON.parse(event.data));
+  };
+};

 const loadInitialData = async setMessages => {

This creates a function setUpWebSocket that ensures our WebSocket is ready to go. If the WebSocket is not already opened, it opens it and hooks up some logging. Whether or not it was already open, we configure the WebSocket to pass any message it receives along to the passed-in addMessage function.

Now, let’s call setUpWebSocket from our component function:

   useEffect(() => {
     loadInitialData(setMessages);
   }, []);

+  useEffect(() => {
+    setUpWebSocket(newMessage => {
+      setMessages([newMessage, ...messages]);
+    });
+  }, [messages]);
+
   return (
     <View style={{ flex: 1 }}>

We call setUpWebSocket in a useEffect hook. We pass it a function allowing it to append a new message to the state. This effect depends on the messages state.

As a result of these dependencies, when the messages are changed, we create a new addMessage callback that appends the message to the updated messages array and then we call setUpWebsocket again with that updated addMessage callback. This is why we wrote setUpWebsocket to work whether or not the WebSocket is already established; it will be called multiple times.

With this, we’re ready to give our WebSockets a try! Make sure you have both Node services running in different terminals:

$ node web
$ node workers

Then reload our Expo app:

  • In the iOS Simulator, press Command-Control-Z to bring up the developer menu, then tap “Reload JS Bundle”
  • In the Android Emulator, press Command-M to bring up the developer menu, then tap “Reload”

In yet another terminal, send in a new message:

$ curl http://localhost:3000/webhook -d "this is for WebSocketyness"

You should see the message appear in the Expo app right away, without any action needed by the user. We’ve got live updates!

What’s Next?

Now that we’ve proven out that we can get live updates to our app, we should move beyond our simple webhook and get data from real third-party services. In the next part, we’ll set up a webhook to get notifications from GitHub about pull request events.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project