Hipflask: offline first real time collaboration for ClojureScript
So I finally got around to properly releasing Hipflask, a little library I've been using in NyanCAD for a while now.
The idea is simple: what if your PouchDB database just acted like a regular Clojure atom? You swap! it, you deref it, you add-watch it. Hipflask handles all the sync stuff behind the scenes.
(def todos (pouch-atom db "todos"))
(swap! todos assoc "todos:1" {:text "Buy milk" :done false})
That's it. It syncs to PouchDB, and if you've got CouchDB set up, it syncs there too. Multiple users can edit the same data and everyone stays in sync. It works offline and catches up when you reconnect. Great for building local first apps, multiplayer experiences, or anything where you need shared state across clients.
The interesting bit is how it handles conflicts when two people edit the same thing at once. Instead of going the CRDT route (which I wrote about before and have some issues with), Hipflask just retries your swap function with the latest data when there's a conflict. So if two people increment a counter at the same time, both increments actually happen. Your application logic decides what "merging" means, not some generic algorithm.
I've been using this to power the real time collaboration in NyanCAD's circuit editor, where multiple engineers can work on the same schematic simultaneously, and it's been solid. It works great with Reagent and Rum if you're building reactive UIs. There's also a silly Global Cookie Clicker example in the repo where everyone in the world shares one cookie counter, if you want to see the multiplayer sync in action.
Check it out: https://github.com/NyanCAD/hipflaskthanks!
@mail985 ^
I am currently looking to use PouchDB in a CLJS project and stumbled on this from searching "CouchDB" here in Slack. Very cool! I probably would have written a wrapper like this myself had I not found it. > Hipflask just retries your swap function with the latest data when there's a conflict. So if two people increment a counter at the same time, both increments actually happen. Interestingโam I reading this right to mean that it's essentially last-write-wins? > NyanCAD I know nothing about circuit design (or hardware generally), but this looks like a really neat piece of software ๐.
I'd be curious to hear if you've found any sharp edge to PouchDB for this kind of use case. It seems so well-suited to the modern, offline-first, realtime-sync sort of application that I'm surprised to have not seen it used more (or heard about its limitations/gotchas). The main caveat I've heard is that initial syncs can be quite slow. Is that true in your experience?
One other question @pepijndevos (and I do apologize for the deluge): I notice in your repo's cookie counter example, you've got an interval calling .sync every 5 seconds. I assume that's not necessary if you call an initial call to .sync with :retry and :live as true (like this: (.sync local-db remote-db #js{:live true :retry true}))? At least, that's my reading of the PouchDB docs, but was curious why you did it like that.
Ohi!
It's last write wins if you just assoc a document. If you apply a more intelligent update it will retry the update if there is a conflict
It has been surprisingly smooth. It's such a nice match for atoms. There are a few minor browser and json limitations like string keys, maximum parallel connections,etc
Maintenance is a bit slow but that seems not unusual in clojure either
Initial sync is slow if there is a lot to sync so that's a matter of app design
The cookie clicker example is years old at this point so I don't recall but what you say sounds right