Synchronisation between a client and a server has been a problem that I have been brewing on in the back of my mind for a long time now. Today I am releasing a Haskell library that helps with exactly this problem.

Synchronising information between two parties is a non-trivial problem. Let us consider simple textual notes as an example.

If two parties can both create notes, then synchronisation involves sending over new notes, deleting notes that have been deleted remotely, etc. Most importantly, synchronisation also requires solving the problem of what should happen when two parties both modify a note. Now we must choose some strategy to merge the modifications to decide on which result to keep and synchronise. This is problem that requires careful nuanced consideration, as anyone who has used git will surely realise. However, when we make the assumption that modification does not occur, then the problem of merging modifications disappears.

In the case of notes, this could be a valid assumption depending on your application. A real world example of such a case is Intray. Intray items can be added and deleted, but never modified.

I wrote a little library that deals with merge-free synchronisation generically. The mergeless library is on hackage and on GitHub. The rest of this blogpost is about the API, the internal details are for another blogpost.

Tutorial

Let us consider an application as follows. A central server stores textual notes, and multiple clients can add or delete (but not modify) items and synchronise the items with the server.

+----------+ +--------+ +----------+ | Client 1 | <-[Sync]-> | Server | <-[Sync]-> | Client 2 | +----------+ +--------+ +----------+

Specifically, we choose the items to be of type Text , and we need to choose how we will identify these items using a separate identifier. We can use UUID , Int , or anything that can be generated to be unique at the server-side. For this example, we will choose Int .

Server-side

On the server, we set up an endpoint that can respond to synchronisation requests. We will leave the specifics of the boundary up aside for now, and focus on the processing of the requests.

Using mergeless , a client will send a SyncRequest Int Text and it expects the server to respond with a SyncResponse Int Text . You can implement this processing manually, or you can use the processSync function provided by mergless .

processSync :: (Ord i, Ord a, MonadIO m) => m i -> CentralStore i a -> SyncRequest i a -> m (SyncResponse i a, CentralStore i a)

The server will need to keep a CentralStore Int Text that contains the items. It also needs to be able to generate unique Int values. This is the m i argument to processSync . To generate unique Int values, the server should also keep the last Int that was generated, and increment that every time. The processSync function will use IO to figure out the time stamp for synchronisation, but you can also supply it manually with the processSyncWith function.

That is all for the server-side. Let us have a look at the client-side as well.

Client-side

On the client-side, we will keep a Store Int Text of the items. A client can get started with an emptyStore .

To perform a single synchronisation step, a client must first produce a SyncRequest Int Text using the makeSyncRequest function.

makeSyncRequest :: (Ord i, Ord a) => Store i a -> SyncRequest i a

Note that nothing other than the store is necessary to make a synchronisation request.

When the server sends back its SyncResponse Int Text , the client only needs to update its local store using the mergeSyncResponse function.

mergeSyncResponse :: (Ord i, Ord a) => Store i a -> SyncResponse i a -> Store i a