Push notifications in Grails and React.js based web projects

This article is more technical oriented and targeting other developers, not the final client. Usually, systems require communication from the server to the client. This is essential because some events occur on the server and a notification must be sent to the client. A simple use case is a chat, where users can exchange messages between them. We can use other technologies too, for example, the Message Queue system. Depending on your needs you can choose the most suitable one.

Description

In this post we will discuss push messages using WebSocket. My goal is to describe the pedagogy behind the work, when it comes to delivery and production, we obviously have simpler routines at the company to accommodate each client order.

Goals 

To be able to implement push notifications in web applications

Requirements 

Grails 3.* (Dependencies: Grails Spring WebSocket, Grails Spring Security Rest)

React.js (Dependencies: Sock.js client, Stomp.js)

1 – Server configuration

We need to configure our WebSocket message agent to allow our system to send and receive messages through secure channels.

The following code (configureClientInboundChannel method) shows how to authenticate through WebSocket. We need to be sure that only an authorized user can connect, subscribe and receive messages from our server. Our system uses Spring Security Rest and, consequently, users are authenticated through the access token in each request.

We also configure the endpoint in which our message agent will listen to subscription requests and messages (registerStompEndpoints method)

import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.SimpMessageType
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.messaging.support.ChannelInterceptorAdapter
import org.springframework.messaging.support.MessageBuilder
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.messaging.Message
import org.springframework.messaging.MessageChannel
 

@Configuration
@EnableWebSocketMessageBroker
class DefaultWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

   public static final String AUTHORIZATION = 'Authorization'
   public static final String BEARER = 'Bearer'

   @Autowired
   TokenStorageService tokenStorageService
 

@Override
void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
   stompEndpointRegistry.addEndpoint("/ws")
         .setAllowedOrigins("*")
         .withSockJS()
         .setSessionCookieNeeded(false)
}
 

void configureClientInboundChannel(ChannelRegistration registration) {
   registration.setInterceptors(new ChannelInterceptorAdapter() {
      Message<Object> preSend(Message<Object> message,  MessageChannel channel) {
         String token = null
         Boolean access = false
         StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message)
         List tokens = accessor.getNativeHeader(AUTHORIZATION)
         accessor.removeNativeHeader(AUTHORIZATION)

         if(tokens?.size > 0) {
            token = tokens[0]
         }

         if (token?.startsWith(BEARER)) {
            token = token.substring(BEARER.length()).trim()
         }

         switch (accessor.messageType){
            case SimpMessageType.CONNECT: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
            case SimpMessageType.SUBSCRIBE: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
         }
         accessor.setLeaveMutable(true)
         return (access)?MessageBuilder.createMessage(message.payload, accessor.messageHeaders):null
      }
   })
}

2 – Sending notifications

We use Grails Spring WebSocket and it will provide us with the necessary functionality ready to use according to the configuration.

In our example, the messages will have a related action, in order to delete, update or create messages in the client. This is also possible if we create different topics for that purpose, but in this case it is not relevant.

BrokerService will be responsible for sending messages to a destination using brokerMesssagingService.

class BrokerService {

    def brokerMessagingTemplate
    /**
     * Send message to destination
     * @param destination
     * @param message
     */
    void send(String destination, Map message) {
        brokerMessagingTemplate.convertAndSend destination, (message as JSON) as String
    }
}

3 – Web client configuration

In the web client we use Sock.js and Stomp.js to subscribe to our messaging channel.

To subscribe to a channel in the connection request, we send the header configuration with the user’s token as authorization.

We set as parameter a callback function that will be executed after receiving a message from the server.

The unsubscribe method allows you to delete a subscription, the user will not receive anymore push notifications afterwards.

import {SERVER_URL} from "../config";
import headers from "../security/headers";
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

/**
 * WebSockets support
 */
const WebSocket = (endpoint) => {

    const socket = SockJS(`${SERVER_URL}/${endpoint}`);
    const client = Stomp.over(socket);

    function subscribe(destination, callback) {
        client.connect(headers(), function(frame) {
            client.subscribe(destination, function(message){
              callback(JSON.parse(message.body))
              }, headers());
           });
        return client;
    }

    function unsubscribe(client){
        for (const sub in client.subscriptions) {
            if (client.subscriptions.hasOwnProperty(sub)) {
                client.unsubscribe(sub);
            }
        }
    }

    return {
        subscribe,
        unsubscribe
    }

};

export default WebSocket;

4 – Subscription and reception of notifications

We will place the subscription call on componentDidMount using a class based on React Component.

We also need to implement the functionality that will be executed once we receive the notifications.

import React from 'react';
import WebSocket from "../../api/WebSocket";
import {ACTION_STATUS} from '../../constant';
import MessageList from "../../components/MessageList";
import { notifyAddedMessage, notifyDeletedMessage,  notifyUpdatedMessage} from "../../actions/actions";


/**
 * Real-time messages
 */
class MessagePanel extends React.Component {

    componentDidMount() {
        this.subscribe();

    }

    componentWillUnmount() {
        if(this.state.client !== undefined)
            WebSocket('ws').unsubscribe(this.state.client);
    }

    subscribe() {
        const callback = (message) => {
            switch (message.action){
                case ACTION_STATUS.DELETED:
                    this.props.notifyDeletedMessage(message.id);
                    break;
                case ACTION_STATUS.UPDATED:
                    this.props.notifyUpdatedMessage(message);
                    break;
                default:
                    this.props.notifyAddedMessage(message);
            }
        };
        callback.bind(this);

        let client;
        if (user instanceof Object) {
            client = WebSocket('ws').subscribe(‘/topic/messages’, callback);
            this.setState({client});
        }
    }

    render() {
        return (
            <MessageList/>
        )
    }
}

Conclusion

Push notifications make our system more attractive and useful, it also reduces server latency and improves user experience. In many cases, we can add it using WebSockets in an easy way and with good results.

References

https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html

 

/Carlos Senior Developer at Visionmate

All articles