Homepage

Using WebSockets with platformOS

Last edit: Jan 24, 2022

In this guide we will show you how to integrate WebSockets into your application based on the chat functionality we implemented for the pOS Marketplace Template.
WebSocket is a protocol for real-time communication between a client and a server. It allows to build an interactive UI where data can be updated from the server without the need of doing http requests. Each client can subscribe to multiple channels and rooms. Messages sent to a certain room are then broadcasted to all subscribers of that room. The pOS implementation of Websockets uses ActionCable.

Requirements

To understand this topic, you should be familiar with:

Steps

Step 1: Setup

Make sure you have added the @rails/actioncable package "@rails/actioncable": "^6.0.3-2" to your package.json file. This package handles communication between the server and the client.

Step 2: Create a connection

Consumers require an instance of the connection on their side. This can be established using the code below.

// modules/chat/src/js/consumer.js

import { createConsumer } from "@rails/actioncable"

export default createConsumer('/websocket')

Step 3: Subscribe to a channel

A consumer becomes a subscriber by creating a subscription to a given channel:

// modules/chat/src/js/chat.js
import consumer from "./consumer";

const chat = function(){

  // cache 'this' value not to be overwritten later
  const module = this;

  module.createSubscription = () => {
      module.channel = consumer.subscriptions.create(
      {
        channel: 'conversate', //channel name
        room_id: module.conversationId, //room ID
      }
    )
  }
}

Step 4: Handle callbacks

There are 4 callbacks handled by the subscription that allow you to react on certain events. You can add handlers for them when you create the subscription object:

// modules/chat/src/js/chat.js
import consumer from "./consumer";

const chat = function(){

  // cache 'this' value not to be overwritten later
  const module = this;
  module.createSubscription = () => {
    module.channel = consumer.subscriptions.create(
      {
        channel: 'conversate',
        room_id: module.conversationId
      },
      {
        received: function(data){
          // function responsible for handling new message
          module.showMessage(data);
        },

        connected: function(){
          // on connect we want to enable the message input
          module.settings.messageInput.disabled = false;
          module.settings.messageInput.focus();
        },

        rejected: function(){
          // when connection has been rejected we want to notify the user about the error
          module.blocked();
        },

        disconnected: function(){
          // when consumer has been disconnected we want to notify the user about the error
          module.blocked();
        }
      }
    );
  };
}

Step 5: Secure room subscription

To make sure that only authorized users can access certain rooms (for example, rooms for conversations where they are one of the pariticpants) you can use a partial that will be called when the user tries to subscribe to a certain room.
This file should be stored in views/partials/channels/:channel_name/subscribed.liquid. In this example, we use code from the pOS Marketplace Template and the channel is called conversate. This partial needs to return a true string if the current user is authorized to subscribe to the room.


{%- comment -%}
modules/chat/public/views/partials/channels/conversate/subscribed.liquid
{%- endcomment -%}

{% liquid
  function current_profile = 'lib/current_organization_profile', user_id: context.current_user.id
  assign room_id = context.params.room_id
  function conversation = 'modules/chat/lib/queries/conversations/find_by_participant', id: room_id, participant_id: current_profile.id

  if conversation
    echo 'true'
  else
    echo 'false'
  endif
%}

In the pOS Marketplace Template, each user has a profile and the profile is linked with a conversation. If the conversation is found then we return true to confirm that the user can subscribe to this room.

Step 6: Send messages

To send a message, you need to call the send method on the subscription object you have created. This method takes Object as an argument.

// modules/chat/src/js/chat.js

module.sendMessage = (message) => {
  let messageData = {
    message: encodeHtml(message),
    author_id: module.settings.currentUserId,
    sender_name: module.settings.messageInput.getAttribute('data-from-name'),
    created_at: new Date(),
    create: true
  };

  module.channel.send(messageData);
};

Step 7: Set server callback on received message

You may want to trigger some code when the server receives a message from a client. To do so, you need to create a partial that will be called when the message is received. This file should be stored in views/partials/channels/:channel_name/receive.liquid. In this example, we use code from the pOS Marketplace Template and the channel is called conversate.

context.params contains all fields that have been sent in the previous step.


{%- comment -%}
modules/chat/public/views/partials/channels/conversate/receive.liquid
{%- endcomment -%}

{% liquid
  function current_profile = 'lib/current_organization_profile', user_id: context.current_user.id
  assign room_id = context.params.room_id
  function conversation = 'modules/chat/lib/queries/conversations/find_by_participant', id: room_id, participant_id: current_profile.id
  assign message_safe = context.params.message | raw_escape_string
  assign object = '{}' | parse_json
  hash_assign object['conversation_id'] = conversation.id
  hash_assign object['autor_id'] = current_profile.id
  hash_assign object['message'] = message_safe
  function message = 'modules/chat/lib/commands/messages/create', object: object
  if message.valid != true
    log message, 'ERROR receive message'
  else
    function res = 'modules/chat/lib/commands/conversations/mark_unread', conversation: conversation, current_profile: current_profile
  endif
%}

In this example, we are retrieving the conversation object and creating a message object to persist data in the database.

Other examples

In the pOS Marketplace Template, we have also implemented a notifications functionality that uses WebSockets to inform user about unread messages. You can find it in the file modules/chat/src/js/notifications.js.

Q&A

  1. Socket timeouts — how does it handle reconnect? Is that the job of the client or can the server reconnect in a permanent listen mode?

The JS client @rails/actioncable is responsible for reconnecting.

  1. Can the websocket service connect to anything else (a notification service to receive events)?

Yes, if that service handles websocket connections.

  1. Timeouts

There is 5 seconds timeout for handling subscribed.liquid and receive.liquid partials.

  1. How do you define a message?

Message is a JSON object.

  1. Parallel processing, assuming just one connection, but how does the server keep up if there are streams of events one after another,
    does it split off thread / processes in the background to cover this or queue this somehow?

Each client has their connection and each connection is handled by a thread on the server side.
If there are multiple incoming messages from a single connection then they are queued.

Contribute to this page

Github Icon

Questions?

We are always happy to help with any questions you may have.

contact us