WebSocket Application

A Simple WebSocket Application in Lolo Code

A webSocket is mainly used to create real-time applications, such as chat apps, collaboration platforms and streaming dashboards. These applications take advantage of two-way/bidirectional communication between a server and users’ browsers.

Using Lolo for applications that are reliant on websockets is ideal. The platform doesn’t require initialisation before the function executes so you don’t pay a price for waking up a container. You also have access to a baked in key / value store which means that you don’t end up loosing state between subsequent invocations. I.e. you can store data within the Application, you don't have to start from a blank slate on every invocation.

We’ll demonstrate something very simple here by using a pre-made Websocket Trigger to connect and broadcast all messages to all connected clients. You can create your own Websocket Trigger by extracting httpServer from the context (ctx) but it is not necessary for you to understand how the Websocket Trigger functions for you to work with this in Lolo.

16801680

Add the WebSocket Trigger

To begin, we need to set up a new websocket server that can handle inbound websocket requests from clients. We do this by first creating a new application and then adding the lolo/Websocket Trigger from the Functions Gallery on the left of the graph. Add in a path name, such as /socket. You can also rename it.

12801280

Process Incoming Request

When you add this WebSocket Trigger you get three output ports called req, message and close so you need to setup ways to process these requests. Add another inline Function and name it 'Process Request.' Remove the in port and instead add in three in ports called req, msg and close.

Link the ports to the right in and out ports with the nodes, look below for an illustration.

12801280

We need a bit of code to do something here with the websocket connection. Copy and paste in the code from below exactly as is in the code editor of the new Function you just created.

const connections = {};

exports.handler = async (ev, ctx) => {
  const { route, input, inputs, log, emit } = ctx;
  const { sessionId } = ev;

  // check incoming port (i.e. req, message or close)
  switch (input) {

    // on first connection
    case inputs.req:
      connections[sessionId] = {
        send: body => emit('response', { body }),
        end: () => emit('response', { end: true }),
        info: ev.headers
      }
      ev.body = { connected: true, yourConnectionId: sessionId };
      // re-route data to 'req' output port
      route(ev, 'req');
      break;

    // on subsequent messages 
    case inputs.msg:
      // re-route data to 'msg' output port
      route({ connections, message: ev.message, sessionId }, 'msg')
      break;

    // when client disconnects 
    case inputs.close:
      log.info("client has disconnected");
      delete connections[sessionId];
      break;
  }
};

The code above is checking what ports the incoming data is routed through, giving us a way to handle a new connection by adding it to connections. We are also setting up how we want to handle subsequent messages and a disconnecting client by using the in ports, msg and close.

This code above though is re-routing to other nodes via the route() method as well. As you can see we have two output routes here to 'req' and 'msg.' We thus need to add those two output ports as well.

12801280

Send back a response

Now we have two outports in the Process Request Function that needs to go somewhere, so we create another inline Function called Affirm Connection and paste in the code below. All this does is signal the listener within the WebSocket Trigger to send a response to the client. We will route the req route to this node.

exports.handler = async(ev, ctx) => {
  const { emit, log } = ctx;
  // Log to the console that a client has connected
  log.info("client has connected");
  // send response to the client
  emit('response', ev);
};

Along with this we also need a way to handle subsequent messages. In this case we said we would broadcast all messages to all clients when someone sends something. So, create another inline Function and call it Handle Messages. Paste in the code below, see comments to understand what it does.

exports.handler = async (ev, ctx) => {
  // extract connections, and current session id from the event data
  const { connections, sessionId } = ev;
  // send messages
  await broadcastToAll(ctx, connections, sessionId, ev);
};

const broadcastToAll = async ({ log }, connections, currentSessionId, ev) => {

  // loop through connections
  Object.keys(connections).forEach(sessionId => {
    try {
        // send message to everyone but the current sessionId
        if (currentSessionId !== sessionId) {
        connections[sessionId].send(`${sessionId} says: ${ev.message}`)
        } 
    } catch (e) {
      log.error(e)
    }
  })

};

You can remove the out ports for both of these new Functions. Now we also have to route the data visually from the Process Request node to the other two nodes we've created.

12801280

Save and Deploy

Alright, that was it. You can save and deploy. Look in the Logs for the 'Listening to port 4000' message to see if it is ready to use. Now go into the WebSocket Trigger to collect the External URL. We are going to use it to connect to this Websocket.

Open two terminals on your computer and then connect via wscat.

wscat -c wss://eu-1.lolo.co/:appId/socket

Remember to npm install wscat -g first if you don't already have wscat installed.

12801280

Adding Authentication

Our web socket application works but anyone who knows the url can connect with this Websocket. We can add simple auth here by using a library function that has been set up with Lolo.

Please read about Lolo's authentication here if you want to understand this Lolo Function in more detail.

We want to authenticate all incoming requests. To do that we need to insert lolo/Lolo API Auth between WS /socket (req port) and Process request (req port).

13251325

From now on, anybody who wants to connect with our Websocket must provide a lolo-api-key as a Websocket query param. See exact syntax below. Notice single quotes around the url.

wscat -c 'wss://eu-1.lolo.co/:appId/socket?lolo-api-key=<user-api-key>'

If a lolo-api-key is not provided or the provided key is invalid, the lolo/Lolo API Auth will throw an error and the connection will be closed. If a valid lolo-api-key is provided the lolo/Lolo API Auth will provide us with a session object with authenticated user details (accountId & email) and the connection will be accepted.

As this is authentication and not authorization, anyone with a Lolo API key will be able to access this connection. However, as the session object provides you with data on the user you can use this data to authorize only some users. Please read about Lolo's authentication here for more information.

Let's try this out by grabbing the email of the session object that the Lolo API Auth library function provides us with to demonstrate the possibilities. This is only a demonstration of the data you can access when you use the library function.

The changes we are making is to assign email address from ev.session to the connection details as a username. Open Process request function and replace the code with following.

const connections = {};

exports.handler = async (ev, ctx) => {
  const { route, input, inputs, log, emit } = ctx;
  const { sessionId } = ev;

  // check incoming port (i.e. req, message or close)
  switch (input) {

    // on first connection
    case inputs.req:
      connections[sessionId] = {
        send: body => emit('response', { body }),
        end: () => emit('response', { end: true }),
        info: ev.headers,
        username: ev.session.email // user email address provided by Lolo API Auth
      }
      ev.body = { connected: true, yourConnectionId: sessionId };
      // re-route data to 'req' output port
      route(ev, 'req');
      break;

    // on subsequent messages 
    case inputs.msg:
      // re-route data to 'msg' output port
      route({ connections, message: ev.message, sessionId }, 'msg')
      break;

    // when client disconnects 
    case inputs.close:
      log.info("client has disconnected");
      delete connections[sessionId];
      break;
  }
};

Now we need to update Handle message function with the assigned username. Please open up this function and paste in this alternate code.

exports.handler = async (ev, ctx) => {
  // extract connections, and current session id from the event data
  const { connections, sessionId } = ev;
  // send messages
  await broadcastToAll(ctx, connections, sessionId, ev);
};

const broadcastToAll = async ({ log }, connections, currentSessionId, ev) => {

  // loop through connections
  Object.keys(connections).forEach(sessionId => {
    try {
        // send message to everyone but the current sessionId
        if (currentSessionId !== sessionId) {
        const { username } = connections[sessionId];
        connections[sessionId].send(`${ username } says: ${ev.message}`) // use provided email
        } 
    } catch (e) {
      log.error(e)
    }
  })

};

That's all. Now you need to save and run your new version.

Try it out by opening two connections and connect with your unique URL found in the Websocket Trigger. Remember to add single quotes around the connection string.

wscat -c 'wss://eu-1.lolo.co/:appId/socket?lolo-api-key=<user-api-key>'
13251325

This is a demonstration on how you can use Lolo Auth but you are free to implement any other auth function to authenticate your users.