Sunday, April 28, 2024
HomeRuby On RailsUtilizing Server-Despatched Occasions (SSE) to Stream Knowledge in Rails

Utilizing Server-Despatched Occasions (SSE) to Stream Knowledge in Rails



Abstract patterns

Picture by Pawel Czerwinski on Unsplash

Often, a web page sends a request to the server to obtain new information. That’s how most of us develop the online these days. What if I instructed you there’s one other approach to get the information to the web page? With server-sent occasions, a server can ship new information to an online web page at any time by pushing messages to the online web page.

We’re all used to the traditional method of how HTTP works:

  1. The shopper sends a request to the server.
  2. Then, the server picks up the request and sends out a response.


Regular HTTP communication diagram

At present, we are going to present the right way to use server-sent occasions (SSE for brief) to determine a special kind of communication between the shopper and the server. Right here’s how SSE works briefly:

  1. A shopper requests a webpage from a server utilizing the common HTTP we confirmed earlier than.
  2. The shopper then receives the requested web page and executes the JavaScript on the web page, which opens a connection to the server.
  3. The server sends a number of occasions to the shopper when there’s new data accessible. These occasions will be despatched at completely different occasions e.g. they are often streamed to the web page.

Right here’s how that appears:


Server-Sent Events diagram

So SSE is designed for a unidirectional information circulation. There isn’t a communication from the shopper to the server aside from the preliminary request.

Cool, now that we coated the fundamentals of how SSE works, we are able to go on and construct one thing utilizing server-sent occasions. We’ll use Ruby on Rails and a little bit of JavaScript to stream information from the server to the shopper. Let’s leap in.

Organising

First off, we’ll create a brand new app known as rails-sse:

I did that with Rails 7.0.6 and Ruby 3.1.1. Now we are able to enter our app with cd rails-sse and generate two controllers:

  1. PublicController – for internet hosting our web page
  2. EventsController – for streaming information to the web page

We’ll do it shortly with these two instructions:

bin/rails generate controller Public index
bin/rails generate controller Occasions index

This may generate controller information, in addition to view information – particularly the app/views/public/index.html.erb that we’ll want in a while.

Then, let’s shortly edit config/routes.rb:

Rails.utility.routes.draw do
  get 'occasions', to: 'occasions#index'
  root 'public#index'
finish

This route definition will goal PublicController and its index motion every time we go to the foundation of our app. It should additionally goal requests to /occasions to the EventsController and its index motion. We’ll depart these controllers empty for now and fill their logic as we proceed.

Nice, now, when you run bin/rails server and open http://localhost:3000, you need to get a web page like this:


Public index page with placeholder text

That’s it, let’s get to constructing and streaming our information.

Subscribing to Occasions

Nice, now let’s open app/views/public/index.html.erb and add some JavaScript logic to subscribe to our /occasions endpoint we outlined beforehand:

<h1>Rails Server-Despatched Occasions Demo</h1>

<script>
  const eventSource = new EventSource("/occasions")

  eventSource.addEventListener("message", (occasion) => {
    console.log(occasion)
  })
</script>

And let’s add some logic to our EventsController:

class EventsController < ApplicationController
  embody ActionController::Reside

  def index
    response.headers['Content-Type'] = 'textual content/event-stream'
    response.headers['Last-Modified'] = Time.now.httpdate

    sse = SSE.new(response.stream, occasion: "message")

    sse.write({ message: 'Hello there' })
  guarantee
    sse.shut
  finish
finish

There’s lots to uncover right here, so let’s begin with the view portion within the app/views/public/index.html.erb file. There, the JavaScript code makes use of the EventSource API to determine a connection to the server at /occasions endpoint. It sends a request to /occasions and receives occasions within the textual content/event-stream format.

The connection to the server is open till closed by calling shut() on the occasion supply object. To ascertain the connection, we instantiate the EventSource object. Then, we add a listener for 'message' to obtain an occasion with the kind 'message' from the server. With these two statements, we’re able to load the client-side code. Earlier than that, let’s undergo the server half within the controller.

Inside EventsController we embody ActionController::Reside module that enables it to stream information to the shopper. That’s the place we get the SSE class from, which we use to construct a server-sent occasion stream. SSE.new receives the response.stream and the next choices:

  • occasion – If specified, an occasion with this identify shall be dispatched on the browser. Occasion identify will be essential if we wish to take heed to particular occasions on the shopper.
  • retry – The reconnection time in milliseconds used when making an attempt to ship the occasion.
  • id – If the connection dies whereas sending an SSE to the browser, then the server will obtain a Final-Occasion-ID header with worth equal to id.

In our easy instance, we’ll outline one choice – occasion. Then, we name sse.write that may commit the information and ship it to the shopper. Then, within the guarantee block, we name sse.shut to ensure the stream is closed. It’s essential to name shut in your stream once you’re completed. In any other case, the socket could also be left open perpetually.

Nice, if we open up http://localhost:3000 now and open the console in our browser, we should always see occasions logged within the console like so:


Simple SSE example with messages in the browser console

In case you look carefully, we’re receiving a number of occasions, although we’re initiating only one EventSource connection. Do you may have any clue what’s occurring? Let’s undergo it within the subsequent part, the place we construct on our instance.

Streaming Knowledge from Rails Server

We coated the fundamentals of getting up and operating with information streaming within the earlier part. Now, let’s deal with constructing our instance into a sturdy answer that may serve in manufacturing.

First off, there was a “drawback” in our instance the place a number of requests have been made to the /occasions endpoint. That’s as a result of we didn’t shut the EventStream connection on the shopper. Right here’s a correct approach to shut the EventStream in order that it doesn’t ping the server continually:

<h1>Rails Server-Despatched Occasions Demo</h1>

<script>
  const eventSource = new EventSource("/occasions")

  eventSource.addEventListener("message", (occasion) => {
    console.log(occasion)
  })

  eventSource.addEventListener("error", (occasion) => {
    console.log(occasion)

    if (occasion.eventPhase === EventSource.CLOSED) {
      eventSource.shut()
      console.log("Occasion Supply Closed")
    }
  })
</script>

Right here, we added the error listener that checks whether or not occasion.eventPhase is closed. If that’s the case, we name the eventSource.shut() in order that the connection is absolutely closed between the shopper and server. That is wanted as a result of in our Rails EventsController, we guarantee response.stream.shut known as. Once we shut the stream on the server, the EventSource on the shopper receives an error. If we don’t deal with that error correctly by closing the EventSource on the shopper, it can suppose a real error had occurred, and it’ll retry to hook up with the server, leading to a number of requests to the server.

All in all, ensure the connection is closed each on the server and on the shopper!

Cool, now that we obtained that out of the way in which and cleared it out, let’s make our instance render messages despatched from the server. For that, we’ll construct on our present instance:

<h1>Rails Server-Despatched Occasions Demo</h1>

<part id="occasions"></part>

<script>
  const eventSource = new EventSource("/occasions")

  eventSource.addEventListener("message", (occasion) => {
    const occasions = doc.getElementById("occasions")
    occasions.innerHTML += `<p>${occasion.information}</p>`
  })

  eventSource.addEventListener("error", (occasion) => {
    if (occasion.eventPhase === EventSource.CLOSED) {
      eventSource.shut()
      console.log("Occasion Supply Closed")
    }
  })
</script>

And let’s stream extra information from our server:

class EventsController < ApplicationController
  embody ActionController::Reside

  def index
    response.headers['Content-Type'] = 'textual content/event-stream'
    response.headers['Last-Modified'] = Time.now.httpdate

    sse = SSE.new(response.stream, occasion: "message")

    sse.write({ message: 'Hello there' })

    sleep 2

    sse.write({ message: 'How are you?' })

    sleep 2

    sse.write({ message: 'I'm effective' })
  guarantee
    sse.shut
  finish
finish

Now, if we open http://localhost:3000 we should always see one thing like this:

SSE rendering on the client

Because the web page renders and connection to /occasions is established, occasions come one after the other, they usually get rendered. You’ll be able to simply debug this as properly. In case you open the Community tab in your browser (I’m utilizing a Chromium-based browser Arc) you’ll see occasions coming in like so:

SSE in the Network tab in the browser

Nice, now for the grand finale, let’s add a few controls to our UI.

Controlling EventSource on the Consumer

Let’s add three buttons – Begin, Cease, and Clear. We’ll begin with the view half in app/views/public/index.html.erb:

<h1>Rails Server-Despatched Occasions Demo</h1>
<p>Right here, you'll be able to see the listing of occasions coming from the server</p>

<button id="cease">Cease</button>
<button id="begin">Begin</button>
<button id="clear">Clear</button>

<part id="occasions"></part>

<script>
  let eventSource

  const begin = () => {
    eventSource = new EventSource("/occasions")

    eventSource.addEventListener("message", (occasion) => {
      const occasions = doc.getElementById("occasions")
      occasions.innerHTML += `<p>${occasion.information}</p>`
    })

    eventSource.addEventListener("error", (occasion) => {
      if (occasion.eventPhase === EventSource.CLOSED) {
        eventSource.shut()
        console.log("Occasion Supply Closed")
      }
    })
  }

  begin()

  doc.getElementById("cease").addEventListener("click on", (e) => {
    eventSource.shut()
  })

  doc.getElementById("begin").addEventListener("click on", (e) => {
    if (eventSource.readyState === EventSource.CLOSED) {
      begin()
    }
  })

  doc.getElementById("clear").addEventListener("click on", (e) => {
    const occasions = doc.getElementById("occasions")
    occasions.innerHTML = ""
  })
</script>

And the server will keep the identical. Now, we are able to mess around with the buttons and begin and cease the streaming or clear the rendered messages from the server. Right here’s the way it seems:

SSE controller via UI buttons

Superior, that covers the fundamentals of SSE and the right way to do it in Ruby on Rails with a splash of JavaScript. Earlier than we shut this submit, let’s undergo some gotchas of SSE.

Gotchas

Beware of those conditions when utilizing or contemplating server-sent occasions.

Use Over HTTP/2

Beware when you’re utilizing server-sent occasions through HTTP/1. When not used over HTTP/2, SSE suffers from a limitation to the utmost variety of open connections, which will be particularly painful when opening numerous tabs because the restrict is per browser and set to a really low quantity (6).

Hold CORS in Thoughts

Use the withCredentials choice in EventSource when you’re sending a request to a different server:

const eventSource = new EventSource("https://my-other-server.com/sse", {
  withCredentials: true,
})

If it’s worthwhile to implement CORS in a Ruby on Rails (or any Rack-based) app, take a look at the rack-cors gem.

One-way Communication

Since SSE is supposed for a one-way connection, this implies you’ll be able to’t ship occasions from the shopper to the server. In case you want two-way communication, you’re in all probability going to be thinking about WebSockets.

Shut The Connection

I’ve talked about it earlier than, however I’ll do it right here – shut the SSE connection each on the server and on the shopper. In Rails:

def motion
  

  sse = new SSE(response.stream)

  
guarantee
  sse.shut
finish

In JavaScript:

eventSource = new EventSource("/occasions")

eventSource.addEventListener("error", (occasion) => {
  if (occasion.eventPhase === EventSource.CLOSED) {
    eventSource.shut()
  }
})

Constructed-in Retry Mechanism

By design, you’ll be able to specify the retry choice within the occasions you ship from the server. This means to the browser the time in milliseconds earlier than it retries to attach in case of a failure. Now, that’s a tough phrase – failure, which we’ll cowl in a bit. EventSource shouldn’t be that sensible to acknowledge all conditions the place communication fails between the shopper and the server. Additionally, the factors shouldn’t be the identical between browsers. So it’s best to run your personal retry or “maintain alive” mechanism in case the connection drops.

In case you do determine to depend on the built-in retry of EventSource, you need to take into account the Final-Occasion-ID header. This header is shipped when the browser tries to reestablish the connection to the server. Then, on the server, you’ll be able to learn the Final-Occasion-ID and reply with a correct occasion primarily based on it.

TLDR – you’re higher off rolling your logic for retrying and reconnecting.

That’s it, let’s sum up within the subsequent part.

Summing Up

Right here’s what Server-Despatched Occasions are briefly:

  • one-way communication the place the server sends information to the shopper
  • an effective way to substitute polling for data
  • EventSource is supported within the majority of browsers – caniuse.com reference
  • straightforward to get going, particularly with Ruby on Rails and ActionController::Reside

Hope you realized a factor or two and that the submit helped you make an knowledgeable resolution on the right way to proceed along with your activity. You’ll find the working answer and the code in the GitHub repo right here.

Within the subsequent submit, we are able to cowl the right way to construct a retry mechanism if the connection or one thing else fails when utilizing SSE. In case you favored the submit, share it with your folks and/or coworkers.

Thanks for studying and catch you within the subsequent one.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments