Delivering Real-Time Notifications to over 300K Sellers With Server-Sent Events on Growth Center

Damla Demir
Trendyol Tech
Published in
12 min readMay 12, 2023
Image from www.freepik.com

As the Seller Growth team, our responsibility is to oversee projects aimed at helping sellers grow their stores through projects such as the notification center, growth center, seller badges, and calendar on the seller panel. We assign various tasks and facilitate level advancements to encourage sellers to actively use all the features on the panel, thereby increasing their interactions with customers and the panel.

In this article, Oğuz Ölke and I will discuss how we elevated user experience through server-sent event implementation for real-time updates on the client side of partner.trendyol.com, what challenges we faced, and how we solved and executed them.

What was our need?

In the seller center panel, we give sellers challenges and tasks to complete in order to increase their interaction with the system and customers. We need to notify the sellers when new tasks are assigned or completed. With the help of this feature, from now on, sellers can get live notifications when they have newly assigned or completed challenges. Hence, we aim to increase the gamification level in our platform and seller engagement.

For now, we only use live notifications for challenges and tasks on the growth center, but with this system, we can send notifications to sellers in any situation according to their needs.

Example Notification

What were our implementation options?

There are several options for implementing live notifications in web applications, each with advantages and disadvantages. Here are some of the most popular options:

In today’s fast-paced world, customers have come to expect real-time updates and notifications while interacting with platforms. Traditional methods of transferring data, such as polling, are generally inefficient and can cause delays, leading to a poor user experience. We also considered using Google’s push notifications. However, we decided against it for two main reasons: first, it requires user permission, which can deter users who are hesitant to give access without understanding its purpose; and second, it sends notifications even when the app is not open, potentially disturbing users and negatively impacting their experience. Therefore, we decided between two options SSE or Websockets.

Server-Sent Event (SSE)

Server-Sent Events (SSE) is a web technology that enables a server to push real-time updates to a client over a single HTTP connection. It allows the server to send data to the client as soon as new data is available without the client making continuous requests for updates.

SSE works by opening a long-lived HTTP connection between the client and server, which remains open until the client or server terminates the connection. The server sends data to the client as a text/event stream, which the client interprets and updates its content accordingly.

Let’s look at the advantages and disadvantages of these options.

Advantages of SSE over WebSocket:

  1. Simplicity: SSE is built on top of the HTTP protocol, making it easier to set up and maintain than WebSocket, which requires a separate protocol. SSE is particularly suitable for applications that require only server-to-client communication, like live notifications.
  2. Automatic Reconnection: SSE automatically handles reconnection to the server in case of network disruptions or failures. If the connection is lost, the client automatically tries reconnecting to the server without requiring explicit handling in the client code.
  3. Built-in Message Buffering: SSE has built-in support for message buffering, which means that if a client loses connection momentarily, it can receive missed messages upon reconnection. WebSocket does not offer this feature by default.
  4. Better Scalability: SSE uses the HTTP protocol; scaling using existing infrastructure, such as HTTP load balancers and caching mechanisms, is more manageable.
  5. Event-driven model: SSE is designed with an event-driven model where the server can send events or updates to the client as they occur without the need for the client to request them explicitly. This makes SSE well-suited for scenarios where the server needs to push updates to the client in real-time, such as real-time notifications, live feeds, or progress updates.

Disadvantages of SSE over WebSocket:

  1. Uni-directional Communication: SSE only supports server-to-client communication, while WebSocket supports bi-directional communication. If an application requires client-to-server communication, WebSocket may be a better choice.
  2. Less Efficient Binary Data Transfer: SSE is less efficient in transferring binary data, as it needs to be encoded as text (e.g., Base64 encoding) before sending. WebSocket, on the other hand, supports binary data transmission natively.
  3. Browser Support: While most modern browsers support SSE, there may be limitations with older browsers or specific environments (e.g., Internet Explorer). WebSocket has more extensive browser support.

Which one did we choose?

In conclusion, the choice between SSE and WebSocket depends on the application’s requirements. For server-to-client communication with a focus on simplicity and ease of implementation, SSE with EventSource is a suitable option. For more complex applications that require bi-directional communication or efficient binary data transfer, WebSocket might be the better choice.

As the seller growth team, we have implemented the server-sent event and event source API because we need one-directional communication, and setting up the SSE is more simple.

How to apply the server-sent event in the browser?

The answer is Event Source API. The Event Source API is a web API that provides a way for the client to receive real-time updates from the server. It establishes a long-lived connection between the client and server, allowing the server to send events to the client as soon as they’re available.

In other words, the Event Source API is like the door that allows SSE to do its magic. The Event Source API establishes the connection between the client and server, and the SSE technology uses that connection to push real-time updates to the client.

SSE Work Flow

Let’s take a deeper look at this Event Source API. Here are some common properties of the EventSource object:

  1. url: The web address or URL of the server-side script or endpoint that the EventSource connects to.
  2. withCredentials: The setting that determines whether the EventSource should include any credentials, like cookies or authentication information, when requesting the server.
  3. readyState: The current state of the EventSource. It can be "connecting" when it's trying to establish a connection, "open" when it's successfully connected, or "closed" when the connection is closed, either by the server or due to an error.
EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1; // connected
EventSource.CLOSED = 2; // connection closed
  1. lastEventId: A hint from the server about the last event it sends. It's like a note attached to the final package you received, telling you what was inside, so you can resume from where you left off if needed.
  2. onopen: The fired event when the EventSource successfully connects to the server.
  3. onmessage: The fired event when data is received through EventSource.
  4. onerror: The fired event when something goes wrong with the EventSource, like if it can't connect to the server or if there's an error on the server side.
  5. close(): This is like a way to manually end the connection with the server.

Let’s illustrate the usage with an example.

// server.js

app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
setInterval(() => {
res.write('data: Hello, world!\n\n');
}, 5000); });
app.listen(3000);
// client.js

const eventSource = new EventSource("/events");
eventSource.open = event => {
console.log("Connection was opened");
};
eventSource.onmessage = event => {
console.log("Received event:", event.data);
};
eventSource.onerror = error => {
console.error("Error occurred:", error);
};

When an instance is created from the event source object, a new connection to the server is opened. The request header includes accept: text/event-stream, which tells the requested data should be event stream type, cache-control: no-cache, which means the server does not cache the response. The server sends 200 status codes if the connection is successfully established and the response header includes content-type: text/event-stream, which tells the response data is event stream type.

Response Messages

After the connection is opened, the server sends messages to the client. The onmessage is triggered each time a message arrives. Here is an example response:

event: update
id: 12345
retry: 3000
data: Hello,world!

In this example, the server sends an event with an “update”. The event has a unique identifier, “12345,” assigned using the “id” field. The server has also specified a retry time of 3000 milliseconds (or 3 seconds) using the “retry” field, which means that the client should wait for 3 seconds before attempting to reconnect in case of a lost connection. The actual content of the message is “Hello, world!”, provided in the “data” field. These messages can be seen in the event stream tab on the browser.

EventStream Tab In Network

How to address missed events on connection loss?

When there is a loss of connection due to network issues in a server-sent events (SSE) setup, it’s possible that some messages may not be received by either the server or the client. To address this, SSE provides a mechanism using the “id” field in the message.

The “id” field in messages lets the client track the last event ID received. Upon reconnection, the client includes the value of eventSource.lastEventId in the Last-Event-ID header, enabling the server to resend any missed events. This ensures message integrity despite network issues.

Everything is great so far; you may say we can connect to our server with the event source API. But there is a problem, of course!

The event source API has a significant limitation!

There is no support for adding custom headers to requests, so unfortunately, it is not possible to use the event source API for applications sending tokens in the header. We provide the authorization process on our systems with a JWT token. In other words, to open a connection between the server and the browser, we need to send the token in the header information of the request. How can we exceed this problem? Let’s look at the Event Source Polyfill package.

Implementing Event-Source-Polyfill With Our Needs:

Alternative polyfills, such as event-source-polyfill and fetch-event-source, support custom headers and are fully compatible with the Event Source API. We used the event-source-polyfill JavaScript library that emulates the native EventSource API while adding support for custom headers. Almost all polyfill packages provide the same functionality. We chose the event-source-polyfill package because it has the support of contributors and is easy to implement.

The event-source-polyfill library enables us to establish a secure connection between the client and the server by sending a token for user authentication. It offers several advantages;

1. Flexibility: event-source-polyfill is compatible with various server implementations, allowing for more flexibility in server-side configuration.

2. Custom Headers Support: The library provides an easy way to send custom headers, like tokens for authentication, which is crucial for securing the connection between the client and the server.

3. Broader Browser Support: event-source-polyfill extends the support for EventSource to a wider range of browsers, ensuring a consistent user experience across different devices and environments.

4. Ease of Integration: The polyfill can be easily integrated into existing projects, making it simpler for developers to adopt and maintain.

That’s enough theoretical knowledge; let’s examine some code together.

Event Source Implementation

This section will discuss the implementation details of the EventSource polyfill, focusing on handling heartbeats, catching errors, reconnecting, and sending tokens.

// liveNotificationService.js

async getLiveNotification (onmessage, onerror) {
const eventSource = new EventSourcePolyfill(
`/live-notification`,
{
headers: {
authorization: `Bearer ${token.accessToken}`,
},
}
);

eventSource.onmessage = onmessage;
eventSource.onerror = onerror;
return eventSource;
}

First, the connection is opened with token and URL information to connect to our server, which has implemented server-sent integration. “onmessage” and “onerror” functions are bound to the event source object.

// liveNotificationHelper.js

import liveNotificationService from '@/services/LiveNotificationService';
import store from '@/store';

export default {
async getLiveNotification () {
let eventSource;

const onmessage = async (event) => {
if (data.content === 'ping-pong') return;

store.dispatch('toastNotification/addNotification', data);
};

const onerror = async (err) => {
if (err.status === 401) {
eventSource.close();
eventSource = await liveNotificationService.getLiveNotification(
onmessage,
onerror
);
}
};

eventSource = await liveNotificationService.getLiveNotification(
onmessage,
onerror
);
},
};

liveNotificationHelper.js is used to open the connection and manage incoming success and error messages.

What we need to pay attention to here is the ping-pong message!

Heartbeat Messages

In the context of event sourcing, a heartbeat message is like a regular check-in from an event source to let other components know that it’s still alive and kicking. Just like how a living being’s heartbeat indicates that it’s alive, a heartbeat message from an event source confirms that it’s operational and emitting events.

Imagine a system where events are emitted by an event source and consumed by event listeners. The event source periodically sends a short message, like a “ping” to let the listeners know it’s still active. This “ping” is the heartbeat message. It’s like a friendly “hello” from the event source, saying, “Hey, I’m here and running!”

We send a heartbeat message from the server every 30 seconds to reassure event listeners that the source is still alive and emitting events and to keep the connection intact.

If we do not send the heartbeat message, the event source will close the connection, give the following error after 45 seconds, and try to connect again.

No Activity Error

Catching Errors and Reconnecting:

With the onError callback that listens for error events, if the error status is 401 (unauthorized), we close the current EventSource instance and create a new one, effectively reconnecting to the server with updated credentials.

Unauth error and reconnection
The state and the provided data by SSE connection

When working with Server-Sent Events, it is possible to encounter a 401 Unauthorized error like the above, indicating that you lack the necessary permissions to access the requested resource. In such cases, it is crucial to reconnect and send the lastEventId value provided to you when establishing a new connection. By including the lastEventId, you ensure that you receive any messages associated with the last event before the connection was lost, effectively preventing any potential loss of essential data. This mechanism allows for a more reliable and seamless user experience, ensuring no crucial information is missed during reconnection.

A Side Note

With EventSource Polyfill, the EventStream tab in the network panel appears empty. This is because the polyfill library processes the events internally and does not expose them to the browser’s developer tools. The events are still being received and processed by the application as expected.

Conclusion

As we approach the conclusion of our discussion, it’s worth noting some additional possible applications of SSE:

  1. Progress Indicators: One practical use case for the EventSource API is displaying progress indicators during time-consuming server-side processes. This is especially useful in file upload scenarios, where users often wait for completion without any progress updates. By utilizing the EventSource API, servers can send real-time progress updates to clients, creating a more engaging and informative user experience.
  2. Real-time dashboards: The EventSource API can be employed to build dynamic dashboards that display real-time data, such as monitoring system health, tracking website visitor statistics, or visualizing other relevant metrics. By pushing updates to connected clients, users can access up-to-date information without needing to manually refresh the dashboard.
  3. Real-time voting: The EventSource API can facilitate live voting or polling systems, allowing users to submit their votes and view results in real-time, creating a more interactive and captivating experience.

And, of course, so much more.

In this article, we tried to explain how the EventSource API was implemented to deliver real-time notifications to over 300,000 sellers on Trendyol’s partner platform. By utilizing the power of Server-Sent Events (SSE) and the event-source-polyfill library, we demonstrated how the user experience could be enhanced through efficient server-to-client communication, ensuring up-to-date information and seamless interaction. This solution has the potential to increase user satisfaction and engagement on the platform significantly.

Thank you for reading! We hope you enjoyed learning about our approach and found it exciting and helpful.

Ready to take your career to the next level? Join our dynamic team and make a difference at Trendyol.

--

--