The ramblings of a web developer.

It took a while for me to write this post, but I came to a solution a few days after writing my latest post.

Thinking to migrate to Firebase, I started to dig more under the hood of how Firebase was working. I liked the idea to have data retrieved very fast but was curious how it could send a lot of data under 200ms.

Turns out it doesn't. What it does is keeping a connection alive and receiving what the queue has to offer to that current connection. If data comes between two requests, it is queued and sent once the new request is ready.

I tried to implement something similar, and the result was crazy. I could send a “retrieve” request (GET in a RESTful way) that would respond immediately with an ACCEPTED status code, and once the requested data was ready, it would be published on that long-polling request or pending until that request would be open.

Of course, I had to rewrite quite some code, but it reduced the code I had implemented and made me write something that was clearer. Win-win !

After a few days of working on it, I discovered something problematic. You are limited on how many long-polling XHR request you can have in a browser for a given domain. This means that any user wanting to open many tabs will quickly have issues.

That's when I started to dig more into websockets. I was already aware of this while I was researching long-polling, but I had the misconception that it was not as widely available as it is.

It turns out that websocket is ready and working very well on all browsers. Moreover, I'm using Sanic on the backend, which has a very good implementation of websockets. Migrating the long-polling request to websocket was very easy, and the result was impressive.

So, how does it work ?

It's quite simple in the end.

I always try to have my server-side structure following the REST pattern, the best I can. So I did the same, but adding a realtime layer on top of that.

Concretely, there is only one command that requires data ; It's GET. The other commonly used commands (PATCH, PUT, POST, DELETE) are actions that needs validation and acknowledgement, so you can't fire-and-forget these.

I've added a custom options on my GET functions that, when enabled, will respond immediately with an ACCEPTED status code, and then continue to work by executing the requested work (listing the users, for instance).

Instead of returning them to the client, the results, fetched one by one for faster processing, are sent to a queue along with a “request identifier”.

On the consumer end of the queue, a websocket event is sent to the user with the request ID and the data available. Once all the data has been sent, a “done” event with the request ID is sent to indicate the client the data has been sent.

The functions handling these GET requests and sending the data to the queue is build in a hybrid way : It will send the resulting data one by one to the queue for the websocket, but only if a specific header is present to behave that way (simply, the request-id set by the browser, which will be able to match the responses received by the websocket). If that header is missing, the data is returned as a standard API response in JSON.

Any POST/PUT/PATCH/DELETE requests are synchronous and always respond once the request work has been done, with the updated information (or removed in case of a DELETE). But in parallel, an event is fired to all the websocket connections to notify of any changes.

That way, when a user updates some data, all the other connected users have their data updated in realtime.

Of course, a security layer is added and only the users belonging to the same organization have these updates. This means that an organization won't have access to data from another organization or be notified from their changes.

This solution is close to what Firebase is offering, except that the logic is done on the server side (filtering, ordering, etc) rather than on the client side. But that's just a decision to lower the amount of data the user will handle. Every data received while connected are stored in the memory of the browser and the code listens to any changes (update/delete) and updates them appropriately.

When the user closes the tab and re-open it later, the first connections loads the essential data again and will receive any subsequent changes and apply them.

This is done simply because it would be too complicated to track the database state right before the user closed the tab, and send only the list of changes when they come back. The data sent when re-opening the service is not that much and with current Internet bandwidth, it only takes a few ms to fully load. Moreover, the data loaded is done in the background while the application shows key elements, so when the user access these data, they are already present.

Conclusion

I hope this was interesting. The whole quest of finding an efficient, fast and always-up-to-date interface was quite challenging and made me rethink how we load and access data.

This required me to re-think and re-write how the backend behave, and how the frontend could rely on the browser to speed up the process.

A future iteration of this would be to rely even more on the browser features to store data (IndexedDB for instance) in order to fasten the rendering of the application, but reload everything from the server and update what is needed.

Taking into consideration slow browsers and slow internet connections will be something else to dig into, which gives this project a nice and complex technical thought.

While working on ways to implement a faster and efficient data layer for Fernand (see my last post here), I (re)discovered Firebase and their real-time database called Firestore.

It has a lot of features that any web developer would love, such as Authentication (with many protocols if needed, including emails for password loss, account validation, etc), Offline mode, a really fast query model, and real-time synchronized data across all the services.

The whole set resonates well with what we want at Fernand, which includes being fast (data should be loaded in milliseconds) and reliable (it should never fail).

Firebase solve a lot of these issues, and that's what pushed me to heavily consider it. With it, we won't have to worry about the database being down, slow queries, WebSocket failing, etc...

Moreover, the code required to implement is small and easy to get started, which is a great plus too.

But I'm afraid of the issues related to now having access to the database directly. I can work my way around the restrictions now (not building complex queries for instance). But if tomorrow we need to calculate the number of users based on some advanced criteria, we will have to download the database locally, and import it on another datastore (such as MongoDB) in order to run these processes. It feels cumbersome.

Another problem I'm not comfortable with is duplicate content. With a relational database, you can have the guarantee that one element won't be present twice in your database. With Firebase, there are no unique checks done, so you can have two incoming requests having the same data, and, somehow, end up with two “unique” values.

One other drawback is that I will have to rewrite the entire backend system we've implemented to now use the Firebase DB instead of MySQL. We are still small and starting, so it's not the end of the world, but what we have is working really well, and we will have to test it again with the new structure.

I agree that Firebase is incredibly fast to return data – my tests shows an average of 20ms! – But you can be the fastest in the universe, you won't be enough if your user is loading 5000 tickets with a slow 2G connection! You can't compete with that! Not even Firebase.

So here I am, pondering the pros and cons, and being quite stuck on the matter...

See you next episode for my takes on the subject!

Thanks for reading.

(Fernand is the new helpdesk service we are building at the time of the writing. It's not publicly live yet even though we've already spent 18 months on this. The release is soon!).

When I first developed the frontend part of Fernand, I went the naive way: The API would load each endpoint when needed, by respecting the RESTFul protocol.

That meant loading all the conversations when you'd hit the “/tickets/” page. That meant waiting for all the conversations and contact details to load.

Granted, I've spent a fair amount of time on improving that endpoint, but it still took about a second to render it properly.

But this wasn't the main issue. The main issue is that by doing so, the content (messages, responses, notes, and events) wasn't loaded, because we couldn't know in advance which one you'd open!

So once the loading was done for the listing, we would wait for the user to make a decision before loading what was needed.

When you would load a conversation thread, it would then fetch the API to load the messages, the contact in details (subscription status, latest payments, etc) and also the list of related conversations from that same user.

That loading could take some time – about two seconds – which would result in a bad User eXperience where you would have to wait when switching between tickets.

Not. Great.

That's why I decided to change the way we would load the data. The idea was that we would constantly load the data we expect the user to view before they make any actions. That way, once they click on a conversation, they have all the details needed in a few milliseconds.

The trick in all of that is to take into consideration the limits of what our users might have: We might reach a memory limit if we load too much, we could slow their bandwidth or worse, use all their data allowance!

Here's my discoveries.

First approach: Load the linked data in the same request

My first test was to consider loading all the data related to one conversations in the same request. This forced me to rewrite a bit of the API to return the batch of conversations requested, with extended informations in it (messages, events, notes, contact, related conversations and payments informations)

I implemented a straightforward approach on the API side and I'm pretty sure I could have improved the queries to the database, but I first wanted to see what would be the result.

Loading a batch of 25 conversations per request was taking, on average, 3960ms. Loading 1600 conversations I have on my local machine took about 4 minutes and 4 seconds:

Keep in mind that in order to be useful, there is no need for the user to load the entire 1600 articles! The first 25 batch is enough to get started, and the rest is here in case the user needs it. That means that once the first request is done, the user can start using the app.

... but still, having to wait 4 seconds to get started is too much for Fernand, so we need to improve that.

So here come the second approach.

Second approach: Loading per batch of types

The previous method had a major drawback: We had to wait on the request to finish to start the second one, because of the “cursor” pagination. We couldn't just ask for the n, n+1, n+2 and n+3 items.

Waiting on one long request is not perfect so instead, we rewrote the backend to load the data as flat: Every related instances (such as messages, contacts, etc) would then be loaded by the frontend if/when needed.

Here's how we did it:

When loading the conversation listing, a request asking for the first 25 tickets would be sent. The data would be returned with IDs instead of linked data. Then, the Frontend would push into a “batch” queue every ID needed to be loaded, per type (a queue of IDs for the Contacts, the Messages, the Related conversations, etc). That Batch system would then throw a new network request everytime the queue would have a size of 10 entries – or – when the loader would have finished.

Doing so has tremendous advantages :

  • We would add on the Batch queue only the IDs we don't already have loaded or that aren't already on the queue. That means if we have multiple time the same ID (same Contact for instance), we would only load it once.
  • By loading only once, we reduce the amount of database query
  • We also reduce the size of the payload

So definitely a bigger win.