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:
- The shopper sends a request to the server.
- Then, the server picks up the request and sends out a response.
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:
- A shopper requests a webpage from a server utilizing the common HTTP we confirmed earlier than.
- The shopper then receives the requested web page and executes the JavaScript on the web page, which opens a connection to the server.
- 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:
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:
PublicController
– for internet hosting our web pageEventsController
– 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:
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 aFinal-Occasion-ID
header with worth equal toid
.
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:
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:
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:
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:
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.