Improving User Experience with Server-Sent Events

Improving User Experience with Server-Sent Events

·

5 min read

In the last two posts, we have built a little app that shows the current location and displays the nearest ice cream shops:

The app gets the location data from the GPS module and the ice cream locations via the OpenStreetMap API. This means that there are a lot of external factors which can cause our app to slow down or even stop working. Now, in order to keep the user happy, let's add at least some small feedback which helps the user to understand what is currently going on.

In order to trigger this feedback we will use Server-Sent Events.


What are Server-Sent Events?

In a typical website, the client (for example the front-end of our app) requests the server for content and receives data as response. However, there might be cases when the server needs to push updated data to the client without the client asking for it. For this purpose, server-sent events have been standardized as part of HTML5 by the W3C in 2008. It is supported by all major browsers.

Basically, server-sent events establish a uni-directional connection from the server to the client. It allows to update a specific DOM component without having to reload the whole page. In PicoLisp, we can implement Server-Sent Events with just a couple of lines of code.


Specifying the DOM component in the front end

In the first step, let's define the front end component that should update itself with help of server-sent events. In case of our app, we choose to add some text below the app title. For example, while the request to the OSM API is running, we want to display the text "Fetching Ice Cafes...":

image.png

By displaying this text, the user knows that our app is still waiting for data.


First of all, let's define the DOM element that should receive dynamic updates.

(<h6> '((id . "icecream") "red")) )

With this we define a h6-element with the id "icecream" and the CSS class "red" (which simply prints red text as defined in the pre-installed lib.css) using in-line CSS styling. (Read here for more on in-style CSS).

Now we connect this element to receive server-sent event updates, by using the serverSentEvent function:

(serverSentEvent "id" 'var . prg)

The first argument should be the ID of a DOM component in the page. (...) The ID should be unique within this program session for all serverSentEvent use cases, as it is also used as a key to an internal association table.

The second argument var is a variable which is automatically bound to a socket as soon as the client establishes the event stream connection, i.e. shortly after the page with the embedded call to serverSentEvent is displayed. In essence, this socket is then connected to the DOM component "id".

When that happened, the prg body is executed to set up everything needed for further event sending via the socket in var. var is automatically set to NIL, and the socket closed, when the client closes the connection. Then the server should clean up whatever is necessary.


Basically this means that we need to define a global variable for the var parameter which is used to establish the connection. Let's create a new global called *Sse and add it to our globals list:

(local) (*Sse *IceCreamOsm *Latitude *Longitude *IceList *SupermarketList)

Now we can call serverSentEvents somewhere on our page. The position is not really relevant, but it might sense to keep it close to the connected DOM element to keep the code readable. Since we just want to pass-through some simple status text, we do not need to define any prg. So all we need is to add this line:

(serverSentEvent "icecream" '*Sse)

Defining the server side

The counter part to serverSentEvent is serverSend. As the name already says, serverSend defines when and which data is sent to the client.

serverSend may be called anytime while the connection is open:

(serverSend 'sock . prg)

It sends all output (HTML text) generated by prg to the inner HTML of the component connected to the socket sock.


Let's use it to print out error messages if GPS is not enabled or available, and otherwise the GPS data of the current location.

The task function is checking the position every 6 seconds. Within this check, let's generate a string that is sent to the *Sse socket linked to the icecream component. This string is then pushed to the client and displayed without updating anything else.

(task -6000 1000
   (nond
      ((location?)
         (serverSend *Sse ,"Please enable \"Location\"") )
      ((gps)
         (serverSend *Sse ,"Waiting for location") )
      (NIL
         (setq *Latitude (car @)  *Longitude (cdr @))
         (serverSend *Sse (prin (lat *Latitude) ", " (lon *Longitude))) ) ) ) )

Note: The comma , is a read-macro in case you want to add multi-language support. If you only use one language, it is not needed.


Likewise, we can also add a server-sent update each time any of the buttons is pressed. Since the button press calls a function icecream (defined in lib.l), we can call serverSend there:

(de iceCream (Msg Osm Category Type)
   (serverSend *Sse Msg)
   (ssl "overpass-api.de"
      ...

where Msg is specified in the button press:

(gui '(+Style +Button) "button-icon bg-white mb-5 mx-3"
   T "icecream/img/supermarket.png"
   '(setq *SupermarketList (iceCream ,"Fetching Supermarkets ..." *IceCreamOsm "shop" "supermarket")) ) )

With these small modifications, we receive a much more user-friendly app that gives a minimum of feedback of what is internally going on.


Wrap-up

With server-sent events, we have an elegant and light-weigth way to update some DOM components dynamically without any JavaScript.

Note that unfortunately the map itself cannot be updated this way, because it calls some more complex JavaScript functions that are not updated like this. So if you want to have a dynamic change on the OpenStreetMap (like required for navigation etc.), you have to reload the whole page, for example with the auto function.


The source code for this example can be found here and this is the zip file for PilBox.


Sources