Real-time Web Apps with Zero Lines of JS

Adam Stepinski
Photo by Marc-Olivier Jodoin on Unsplash

In a previous post, I described how Instawork doubled our web development productivity by abandoning React and embracing server-rendered pages enhanced with Intercooler.js. A standard React/Redux setup requires a lot of duplicated work; logic has to be written once on the server (as the source of truth), and a second time on the client (for local state management). By doing all UI rendering in our Django codebase, we eliminate the need to write any custom app logic in JS. Of course, pure server-rendering doesn’t deliver the smooth experience users expect from web apps. That’s where Intercooler.js comes in: by adding a few HTML attributes to our elements, we can support a wide range of interactions by swapping content on the page with AJAX. All rendering is still done server-side, but pages can come alive with interactions like infinite scrolling, tab switching, dynamic multi-step forms, etc.

I always assumed that Intercooler’s swapping mechanism wouldn’t work for some of the more complex UI interactions we planned to add to our app. In particular, I worried how we’d handle real-time features in our product, like chats or streaming notifications. Would we need to give up on the zero-JS dream to implement these features?

I’m happy to report that my worries were unfounded. We successfully shipped realtime features in our web app without writing a single line of JavaScript! The rest of the post will explain how we implemented a real-time status feature using Django, Intercooler, and Server-sent events with Mercure.

Status tags update as the professional arrives at the venue of their gig.

Server-sent events on frontend

Server-sent events (SSE) is a widely-supported web standard that allows browsers to push new data to a web page at any time. Unlike WebSocket, SSE is a simple uni-directional protocol implemented using HTTP on a long-lived connection. Web pages can open a connection to get named events (and event data) using the EventSource interface in JavaScript. Responding to events is done by registering callbacks.

Intercooler has built-in support for SSE that manages the EventSource interface automatically. The syntax looks like this:

<div ic-sse-src=”/dashboard/events”>
<div ic-trigger-on=”sse:status-updated-123" ic-src=”/status/123">
Not arrived
</div>
</div>

This is just regular HTML with two special attributes:

  • ic-sse-src specifies the endpoint to use when creating an EventSource. Since SSE is done over HTTP, the browse will send along the appropriate cookies for authentication.
  • ic-trigger-on specifies the event that will trigger the content swap. The special “sse:” syntax specifies that the given event should trigger a swap.

With these two attributes, our frontend is fully capable of real-time updates! When the frontend receives the event status-updated-123, Intercooler will request content from /status/123. The response will be an HTML fragment containing the new response status. Intercooler then swaps out the status div with the new fragment. And that’s it, the UI updates in real-time in response to events, and we didn’t need to write any JavaScript to do it!

Server-sent events on the backend

Of course, what I described is the easy part. Behind the scenes, we need to implement the long-lived SSE endpoint, as well as a mechanism to push relevant events to all clients. Web frameworks like Django don’t handle long-lived connections very well, so we looked for other options. The Django Channels project extends Django to handle asynchronous communication, but it doesn’t support SSE out of the box. We wanted something that supported SSE in a language & framework-agnostic way.

Enter Mercure. Mercure is an open-source project that provides scalable SSE communication using a centralized Hub server

Mercure Hub fans out events from the server using SSE (Source)
  • Clients connect to the long-lived SSE endpoint on the Hub server. Authorization is done using a JWT token. The token encodes the event topics and event targets for which the client has permission.
  • The server sends events by making a simple POST request to the Hub server with a JWT token. The token encodes the event name, topic, and the targets that should receive the event.
  • Upon receiving a POST request, the Mercure Hub will send the event to all of the open SSE connections that match the topic and targets.

We considered a few options and services for handling SSE connections, but ultimately decided on Mercure for a few reasons. As mentioned above, Mercure is built around SSE, so its language and framework agnostic. Any system that can encode JWT tokens and make HTTP requests can use Mercure. We also appreciate that Mercure is a small micro-service that runs alongside our servers, rather than as a proxy in front of our servers (the approach used by Pushpin). This means there’s less risk to our site if Mercure goes down: we lose real-time features, but the main site will continue to function. Finally, we’ve found it to be very stable and scalable. In our load tests Mercure didn’t break a sweat, even when running at 10x the loads we expect.

Our Django integration with Mercure took the form of a helper function for sending events, and a view mixin. The view mixin does two things:

  • it sets a mercureAuthorization cookie used by the client to connect to the Hub.
  • it adds the Hub URL (with subscribed topics) to the response’s context data, so that it can be rendered as the ic-sse-src attribute.

I’ve shared a Gist with a complete example of our helpers, mixins, views and templates.

Conclusion

Photo by Prateek Katyal on Unsplash

It’s easy to think that features like real-time UIs require a heavy client-side framework. But thanks to Intercooler’s built-in support for SSE, and the simplicity of setting up and integrating Mercure, we’ve been able to stick to our “no JS” philosophy while delivering the features our users expect. We continue to be surprised by the capabilities of Django + Intercooler + Mercure, and adding new realtime features is now a snap.

Have you added real-time updating to your web app? Are you thinking about adding it in the future? We’d love to hear about your experience and solutions, or provide more insights into Django, Intercooler, or Mercure.


View Original