Websocket notifications in django

Question:

I am implementing web socket notifications into my Django project and am having trouble with passing the user the the amount of unread notifications that they currently have.

My solution to this problem was to simply send all objects in the notification model class to the user once they connected to the web socket as messages in the web socket itself, which initially worked, but breaks if there are more then one tab open as each new tab connects to the web socket again which in turn will send the web socket messages again to any other open tab logged in to that user. ending up with duplicates of the same notification.

What I need is a way display all notifications on each page when it is loaded. Preferably without sending it through the context of a view.

consumers.py:

@database_sync_to_async
def create_notification(notification):
    """
    function to create a notification.
    :param notification: notification in json format to create notification object
    """
    user = User.objects.get(username=notification["target"])
    NotificationsModel.objects.create(
        target=user,
        sender=notification["sender"],
        alertType=notification["alertType"],
        title=notification["title"],
        message=notification["message"],
    )

class NotificationConsumer(AsyncWebsocketConsumer):
    async def websocket_connect(self, event: dict) -> None:
        if self.scope["user"].is_anonymous:
            await self.close()
        else:
            self.group_code = self.scope["url_route"]["kwargs"]["group_code"]
            await self.channel_layer.group_add(self.group_code, self.channel_name)
            await self.accept()
            # Since removed the send notifications from here as they encountered the problem described above

    async def websocket_receive(self, event: dict) -> None:
        message = event["text"]  # anything sent to websocket_receive has {"type": "websocket.receive", "text": "foo"}
        type_of_handler = json.loads(message)
        type_of_handler = type_of_handler["type"]
        await self.channel_layer.group_send(self.group_code, {"type": type_of_handler, "message": message})

    async def websocket_disconnect(self, event: dict) -> None:
        await self.channel_layer.group_discard(self.group_code, self.channel_name)
        await self.close()

    # custom message handlers called based on "type" of message sent
    async def send_notification(self, event: dict) -> None:
        await self.send(json.dumps({"type": "websocket.send", "message": event}))

    async def notify_and_create(self, event: dict) -> None:
        message = json.loads(event["message"])
        await create_notification(message["notification"])
        notification = json.dumps(message["notification"])
        await self.channel_layer.group_send(
            str(message["notification"]["target_id"]), {"type": "notification", "message": notification}
        )

    async def notification(self, event: dict) -> None:
        await self.send(text_data=json.dumps(event["message"]))

javascript:

try{
var groupCode = document.getElementById("user");
var user = groupCode.getAttribute('user')
}catch(err){
    throw Error('notifications not active (do not worry about this error)')
}

var loc = window.location;
var wsStart = "ws://";

if (loc.protocol == "https:"){
  wsStart = "wss://";
}
var webSocketEndpoint =  wsStart + loc.host + '/ws/notifications/' + user + '/'; // ws : wss   // Websocket URL, Same on as mentioned in the routing.py
var socket = new WebSocket(webSocketEndpoint) // Creating a new Web Socket Connection
console.log('CONNECTED TO: ', webSocketEndpoint)


var counter = 0;

// Socket On receive message Functionality
socket.onmessage = function(e){
    document.getElementById("no-alerts").innerHTML = "";
    console.log('message recieved')
    const obj = JSON.parse(e.data);
    const object = JSON.parse(obj);
    console.log(object)

    var toastElList = [].slice.call(document.querySelectorAll('.toast'))
    var toastList = toastElList.map(function (toastEl) {
     return new bootstrap.Toast(toastEl)
    })

    counter = counter + 1;
    document.getElementById("badge-counter").innerHTML = counter;

    if (counter < 4) {
        // stuff to show notifications in html
    }
}

// Socket Connect Functionality
socket.onopen = function(e){
    document.getElementById("no-alerts").append("No New Alerts!");
    console.log('open', e)
}

// Socket Error Functionality
socket.onerror = function(e){
  console.log('error', e)
}

// Socket close Functionality
socket.onclose = function(e){
  console.log('closed', e)
}

Any ideas to point me in the right direction?

Asked By: liam

||

Answers:

I’m focusing on the simplest possible solution here, but there is a potential pitfall to my answer, which I describe at the end, so you will need to evaluate if this is manageable for your use-case.

Assuming you want to keep the simplicity of the "bulk send" approach you currently have, you can solve this with minimal modifications by adding an index (a number) to your notifications, and keeping track of the last processed notification index in each tab, only reacting to notifications with higher index values.

By simply incrementing the index by 1 for each distinct notification sent by your backend, your tabs have a way to only display each notification once.

It could be as basic as a new index variable in your NotificationConsumer class that you increment each time in send_notification, and a global variable in your javascript in which you store the last index you processed, something like

var last_index = 0;
...

socket.onmessage = function(e){
    document.getElementById("no-alerts").innerHTML = "";
    console.log('message recieved')
    const obj = JSON.parse(e.data);
    const object = JSON.parse(obj);

    if (last_index < object.index) {
        last_index = object.index;
        //process message...
    }
...

One potential (but improbable) pitfall is if your notifications are sent or received out of order, which would cause some notifications to get skipped. I could not speculate how this could happen via websocket, but if this is a concern, you would need to save a sufficiently large array of the last processed messages on the javascript side to cover the potential sequence break. This is speculation at this point, but feel free to comment if you feel this applies to you.

Answered By: M. Gallant