On the engineering team at Artsy, we've built a CMS for both internal and external editors to write and publish articles. We have a team of a dozen in-house editors creating new content on a daily basis. As many people starting using the app simultaneously, something became apparent. Editors would unintentionally go and override each other’s work because there was no way to tell if someone else was currently editing an article. As a workaround, team members would be forced to edit drafts in another editor such as google docs and copy their work over once ready. This made for a lackluster collaborative experience.
So we decided to implement a system that would make our editors more confident in our CMS by ensuring only one editor could go in and edit an article at any given time. I was tasked with coming up with an elegant technical solution for this feature. Here's the approach I took....
We decided to resolve this issue building an article locking mechanism. When an editor would start editing an article, all other users in sessions would be notified. One of the requirements for this new feature was for things needed to update without refreshing the page. In order to fulfill this, we needed to implement a system to push events from the server to clients.
Based on the requirements presented, I looked at potential solutions for this. Right away, the HTML5 WebSocket API seemed like the perfect solution to keep all clients synced in realtime, however a few issues arose. For one, many proxies and firewalls block WebSocket connections, so it's not always an available option for clients. I needed to find another option to mitigate that problem. That's where socket.io comes in.
Socket.io, a battle tested library for creating real-time bidirectional communication channels, helps mitigate those problems. In a gist socket.io initially establishes a long-polling HTTP connection, and in parallel tries to upgrade it to WebSocket.
So how do we go about integrating socket.io in the Redux-based state architecture we just designed. I thought the best would be to change as little as possible to the code structures developers familiar with Redux are already used to. Namely, use standard Redux actions creators and simply use a decorator to enhance them.
Here's a simplified version of the function decorator which broadcasts redux actions via a socket connection to other connected clients.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
To recap the code above,
emitAction is a function decorator that enhances action creators to dispatch actions via the local store and also broadcast that same action to other connected clients. The following code snippet shows how it's being used to wrap a typical redux action creator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
On the receiving end, we simply add a reducer to process the event from the action payload which we can then return a new state from.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
We can now use our
emitAction decorator function to enhance any number of action creators. All that's needed is to add a
key property to the action object. We can then decide to either process actions
on a backend service or proxy them directly to other clients. You can find the remainder of the server implementation and our event handlers in our github repo along with instructions on how to run the code.
There's an opportunity to extract this module for reuse in other projects and apps. Another logical improvement to this project would be to implement collaborative editing using this architecture. It would also be nice to include helpers for handling events on backend servers.