Implementation of Server-Sent Events and EventSource- Live Progress Indicator using React and Spring Boot
Understand how SSE works with a real-life use case- Loading Percentage Indicator on File Upload made using Spring Boot and React.
If you have this question in your mind- What’s Server-Sent Events? Then I would highly recommend you to go through this article- Getting started with Server-Sent Events.
Here’s brief info about SSE for those who are in a hurry!
SSE is a lightweight protocol on top of HHTP Streaming that allows super light-weight subscribe-only capabilities to clients. Unlike WebSockets, SSE does not provide the capability of two-way communication but can be used by the server to push data to the clients in real-time. SSE enables a client to receive automatic updates from a server via an HTTP connection. It is designed to use the JavaScript EventSource API to subscribe to a stream of data in any popular browser. Through this interface, a client requests a particular URL to receive an event stream.
You might consider using Server-Sent Events when you have some rapidly updating data to display, but you do not want to poll the server. Examples might include displaying the status of a long-running business process, tracking stock price updates, or showing the current number of likes on a post on a social media network.
Now, that you know what’s SSE, I will explain to you the following things-
- What was the Use Case?
- Why did I choose to use SSE?
- What are Events?
- Implementation details.
Use case
The system processes the files extracts certain information and then saves it. Depending on the internet speed sometimes it might be a long wait for the user. So, to make the user experience as pleasant as possible, on file upload, display a real-time progress bar that shows how much the server has processed so far, without hitting endpoints repeatedly in the progress bar.
Why SSE?
The requirement was to display a percentage indicating how much data was processed(parsed/read) in that file and then finally display 100% when the file was saved after the processing was done. So, I needed a way to continuously communicate to the client, sending it the percentage of the file that had been processed.
Server-Sent Event was a great solution for implementing this use case. I had to display rapidly updating data without having to poll the server as it was about the progress for the same HTTP request.
To understand how SSE works, you need to know what are events first.
What are Events?
The server can send multiple events before closing the connection. Messages sent by the server should be text-based and the message starts with a keyword followed by a colon(:) and then a string message. ‘data’ is a keyword that represents a message for a client.
In the case of multiple messages, messages should be separated by a blank line otherwise client will treat them as a single event. The browser concatenates the above four lines and emits one event. To separate the message from each other, the server needs to send a blank line after each message. SseEmitter used for emitting messages will take care of this next line and keyword format when you send multiple messages in Spring.
To process these events in the browser, an application needs to register a listener for the message
event. The property data
of the event object contains the message. The browser filters out the keyword data
and the colon and only assigns the string after the colon to event.data
.
Named Events
A server can assign an event name to a message with the event:
keyword. The event:
line can precede or follow the data:
line. In this example, the server sends 4 messages. The first message is an add
event, the second a remove
event then follows an add
event again, and the last message is an unnamed event.
Named events are processed differently by the client. They do not trigger the message
handlers. Named events emit an event that has the same name as the event itself. For this example, we need 3 listeners to process all the messages. You cannot use the on...
syntax for registering listeners to these events. They have to be registered with the addEventListener
function.
I will be using Named Events for this demo for resolving concurrency issues. Using named events, multiple clients can call my “upload file” service simultaneously without any issue. The progress percentage for each of the clients will be as per their file processing and upload status. Progress percentage of one client won’t mess up the other. Please go through the implementation details to understand how did I achieve this.
Implementation Details
I will be using Spring Boot and React for this demo.
Spring Boot provides a way to implement SSE by using Flux which is a reactive representation of a stream of events, however, in this post, I will use the SseEmitter class of Spring to create an async controller which will emit multiple messages to the client.
Here is the high-level architectural diagram of the application.
General Steps involved in SSE are-
- The client opens an HTTP connection.
- The server can send any number of events(messages) to this connection asynchronously.
- The server can close a connection or it can be closed because of some network error or any exception at the server-side.
- In case the connection is closed because of any error from the server or any network error, the client will automatically try to re-connect
Server-Side Implementation in Spring Boot
Project Structure
I have created an async REST
controller with two methods responding to HTTP requests. One with @GetMapping
annotation and another with @PostMapping
annotation.
private Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
- This map declared at the class level keeps track of the SseEmitters created for each client. I have used ConcurrentHashMap for achieving concurrency and thread-safety.
- In the first method with
@GetMapping("/progress")
annotation I have created an object of SseEmitter with an optional timeout as an argument in the constructor. I have set the timeout to a very large value so that it never times out automatically. This method generates a random UUID, creates an object of SseEmitter, and stores it in the map, with key as the random UUID and value as the object of the SseEmitter. This UUID is used to identify each client uniquely. You will understand how it is done as you read further. - The second method with
@PostMapping("upload/local
annotation is the one that listens to thePOST
request for uploading the files. It receives the file as a Multipart request and a unique UUID as a string. It then retrieves the SseEmitter object from the map(defined in step 1) and passes the object to theFileStorageService
.
4. The FileStorageService
reads the file line by line and runs some internal logic to calculate the percentage. It is here where you will have to write your logic to calculate the percentage and emit it using the object of SseEmitter that was passed to it. For demo purposes, I have written a simple logic to calculate the percentage while the file is being read line by line.
You can define the maximum file size that can be uploaded by writing these two properties in the application.properties
file.
– spring.servlet.multipart.max-file-size
: max file size for each request.
– spring.servlet.multipart.max-request-size
: max request size for a multipart/form-data.
In case of error, you can send a special event with a complete error method. This will instruct the client about the error.
Finally, after sending all the messages, you have to complete the emitter by calling a complete method to close the connection, and then remove it from the map using the UUID key.
Client-Side Implementation in React.js
Events can be handled in any javascript library or framework like react, angular, etc. I have used React for this demo.
Project Structure
I have simply created a react project with create-react-app
and used the EventSource
interface to subscribe to the endpoint as follows:
When the user chooses a file and clicks the upload button, the client opens a connection with the EventSource object with the URL of the server API.
const eventSource = new EventSource("http://localhost:8080/progress");
It also registers an EventListener for an event named GUID
to handle events returned from the server. Meanwhile, the server sends an event named GUID
with data as a unique UUID. This UUID is used to uniquely identify a client. The client captures this UUID and sets up another EventListener of the type of that unique UUID.
For all the successful events from the server, the callback method which is passed as the second argument while registering the event listener will be called with one argument as an event object. Message sent from the server can be retrieved from event object by using property ‘data’, so event.data will hold the message sent by the server. This event.data can be a simple string or a JSON string. If it is a JSON string you have to parse it:
Now, as the file is being read, the percentage is calculated with some logic and that is sent as data to the event of type unique UUID, that was generated when the client first opened the connection.
If during an upload process another client calls the upload API, a new unique UUID is generated, a new SseEmitter object is created and is put in the map. Now, the percentage values are calculated freshly for this call and do not affect the previous ongoing progress bar.
There are two other built-in events, one is ‘open’ which will be called once 200 status is received from the server. Another event is ‘error’, this will be called whenever there is a network error and also when the server closes the connection by calling a ‘complete’ or ‘completeWithError’ method on the emitter. You can close the connection at client-side by calling event.target.close();
, otherwise, the client will keep on retrying the connection in case of error, if you have used .reconnectTime(1000)
while creating the SseEmitters.
I have used the Ant Design Progress Component for the progress indicator.
So, that is how the live percentages are shown on file upload using server-sent events.
Here is the demo of the application.
The complete code can be found in my GitHub repo.