bramw_baserow/docs/technical/websockets.md

7.1 KiB
Raw Permalink Blame History

Working with websockets

Baserow uses Django Channels library to handle websocket connections.

Consumers

The communication between connected clients (like the Baserow web-frontend) and Baserow backend is done through Django Channels consumers. A consumer is akin to a Django view. It can receive payloads from a client and send payloads to the client. The difference is that consumers are stateful and handle communication back and forth for the whole duration of a websocket connection.

Similarly to Django views, consumers are hooked to a particular URL, see this excerpt from backend/src/baserow/ws/routing.py on how the CoreConsumer is routed:

websocket_urlpatterns = [re_path(r"^ws/core/", CoreConsumer.as_asgi())]

The above example shows that any client that wants to establish a websocket connection using the ws/core/ URL (with ws protocol) will be handled by the CoreConsumer.

Each consumer has access to the connections scope which is like the request object in traditional views, holding various information about the connection.

AsyncJsonWebsocketConsumer

We use AsyncJsonWebsocketConsumer from the Django Channels library as the base for our consumers since we want to exchange JSON payloads. These consumers typically have three main event handlers: connect (for setting up the connection or revoking the connection), disconnect (for cleanup), and receive_json (for reacting to client's messages).

In each AsyncJsonWebsocketConsumer, we will typically want to:

  • React to client's messages in receive_json
  • Send messages back to the connected client via self.send_json(..)
  • React to custom events by implementing our own event handlers as class methods, e.g. async def react_to_custom_event(self, event):. Custom events are for handeling messages coming from other consumers or other backend code as opposed to handeling messages from clients.
  • Join channel layer groups via self.channel_layer.group_add(..) to subscribe clients to additional events (more on that below).

Let's have a look at a simple consumer:

class MyConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        await self.accept()

        # We can access the scope object holding connection's information
        # In this case Django Channels will provide
        # authenticated user
        user = self.scope["user"]

        if not user:
            # We don't have to allow the connection to
            # be established.
            await self.close()
            return

        # Join every new connection to the "users" channel group
        # that can be used to later broadcast messages to everyone
        await self.channel_layer.group_add("users", self.channel_name)

    async def disconnect(self, message):
        # Remove the connection from a channel group
        await self.channel_layer.group_discard("users", self.channel_name)

    async def receive_json(self, content, **parameters):
        # Process a message from a client

        # If client sends "Hi", say Hello back
        if "hi" in content:
          self.send_json({"message": "Hello back!"})

    # Event handlers

    async def react_to_custom_event(self, event):
        # To invoke this event we will need to manually
        # send a message to channel layer with this event name
        ...

CoreConsumer

The main Baserow consumer is CoreConsumer (from backend/src/baserow/ws/consumers.py). It currently handles all web-frontend connections, all backend events and exchange of all messages between clients and the backend.

Channel Layer and Channel Groups

In essense, a channel layer facilitates cross-process communication like the communication between consumers themselves or between consumers and any other backend code that needs to send messages to connected clients. Baserow uses RedisChannelLayer for this purpose.

Each consumer has a unique channel name (the self.channel_name in the example above), and can join arbitrary-named groups, allowing both point-to-point and broadcast messaging.

Currently, CoreConsumers use these channel groups for broadcasts:

  • users for all connected clients (includes anonymous users)
  • page groups representing "table", "view", or "row" pages that have been originally created for people browsing these pages to receive real-time updates
  • permission-oriented groups to track consumers that need to listen to permission updates and be able to disconnect from channel groups that they shouldn't be a part of anymore

Pages and Subscriptions

CoreConsumer has a concept of pages that a client can subscribe to in order to receive messages targeting specific pages. Clients have to manually request to be subscribed with a special payload. The consumer can then check if the client has the permissions necessary to receive these page updates and if so, add itself to the particular channel group representing the page.

For example, users can subscribe to receive updates to a particular Baserow table. If the request is permitted, the consumer handling the connection will join table-{id} channel group and start receiving messages related to the table page with the particular id.

Each page that can be subscribed is implemented as a PageType and registered in page_registry so it is possible to implement new page types without making changes to the consumer itself. See backend/src/baserow/ws/registries.py for details.

Message Broadcasting

Often we need to notify connected clients about something. For example, clients subscribed to a table page need real-time updates about created or updated rows.

The main method to send a message to all consumers (all clients) in a particular channel group is through send_message_to_channel_group() function in backend/src/baserow/ws/tasks.py. The message parameter should contain the type parameter referring to the event handler that will be invoked on each consumer:

from baserow.ws.tasks import send_message_to_channel_group
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

channel_layer = get_channel_layer()

message = {
  "type": "react_to_custom_event",
  # ...event payload
}

group = "table-2"

async_to_sync(send_message_to_channel_group)(channel_layer, group, message)

Front-end

Websocket connections are automatically established for each user, including anonymous users, in the main page layout web-frontend/modules/core/layouts/app.vue when the Baserow web-frontend is loaded. Interacting with the backend using websocket connections is abstracted in RealTimeHandler class which is available in Vue components under this.$realtime property.

Consult client-side documentation in docs/apis/web-socket-api.md for implementing webscocket clients for Baserow.