There are many frameworks and solutions for implementing real-time data synchronization within client applications. However, most are extremely cumbersome and require a great degree of extra work in order to interface with existing systems. As such, one possible solution for many of these problems is adopting a uni-directional data flow as the synchronization client’s underlying architecture as it provides a natural solution for many of the problems that come with implementing synchronicity among many application clients.
This proposal is largely inspired by Facebook’s Flux Framework. By departing from the traditional MV* methodology, a number of complexities caused by mutable application data are removed, making the prospect of synchronization much simpler. In essence, this proposal draws on Flux’s “Uni-directional data flow model” which implements the following Observer Pattern:
As outlined in the diagram above, actions are sent to stores containing updates to the application data. Stores maintain the state of application data and update their own state based on received actions. Components are passed an entirely updated state of the data they are interested in and render this data in some manner to the user’s screen. When any interaction involving a change in data within a Component occurs, either by a user or by application logic, a new Action containing the appropriate context for the change in data, is broadcast from the Component and the cycle repeats.
Why This Is Ideal for Synchronization
There are many characteristics inherent in a uni-directional data flow that are ideal for building a synchronization framework on top of. First off is a lack of two-way binding within the framework, or having application models listening directly to views for changes, and inversely views updating directly when a model changes without any controller interaction. As of writing this paper, this is the most common model implemented in many of today’s leading web frameworks including AngularJS created by Google and EmberJS. While this is an effective solution it often results in a lack of scoping within components of the larger system and results in complex views that require a great deal of data to maintain and render.
In a uni-directional model, component updates are architecturally expensive to implement and make passing a global data set to anywhere within the application difficult to accomplish. While this may sound detrimental, there is a major advantage in that it naturally forces developers towards implementing a modular and minimalist funnel of data down into a hierarchy of nested components. In turn this lends itself nicely to synchronization because an update to one component is contained within a small set of contained data unique to that one component.
As seen above, the other major advantage of building synchronization on top of a uni-directional data flow is it’s action oriented manner. This naturally lends itself to a message based synchronization tools such as web sockets or RTC because the actions themselves can easily be serialized and sent to other peers. Upon receiving these foreign actions, the can easily be plugged into the the existing architecture without any additional interpretation or context making it extremely simple for the application to handle.
Initializing and Maintaining Synchronization
One of the biggest challenges with synchronization is providing initial state to a new client that wishes to synchronize with other users in an in progress application state. When dealing with a server-client architecture, the best way to accomplish this by providing the new connection with an initial payload containing the entire base action state. This is typically implemented with a single action that fires as soon as the client is ready to start processing data as designated when the root component of an application view is ready to accept data:
After reaching a synchronized state, the next challenge becomes remaining in such a state. Perhaps the easiest way to manage sending and receiving Actions between peered applications is by sending them to a central backing server who is then responsible for broadcasting received Actions out to all other connected applications. In the event that many events are sent to the server or received by a peer at once, both the broadcasting and execution order will be based on FIFO queue. Below is a straight forward example of what this might look like. Again, the advantage here is that the Action based system that is required in designing a simple uni-direction data flow application can easily be used to transmit changes in context and data to many peers.
While this is the most basic of examples, using this approach is simple enough for the demands of many modern applications. One other alternative is to perform the initial render of the application on the server side which allows you to preset the initial store state directly so no initial AJAX call to load data is required.
Things become considerably more complicated when we remove the central backing server for coordination purposes. Assuming we are using a technology such as WebRTC which is essentially a browser-to-browser communication system over sockets this is still feasible. The first major issue becomes synchronization after making an initial connection. Assuming that the new peer has been provided an ip/port combination that connects to a number of other synchronized peers the following is one solution assuming all Actions sent between peers are also appended with a timestamp at the time they were generated:
Establish a connection and immediately broadcast a “PingAction” asking all peers to broadcast a global unique identifier(GUID) that they generated for themselves upon initializing their local client.
Begin a FIFO queue for all actions received in the interim.
Choose the first responding “PongAction” and immediately send our a “SynchronizationAction” with their GUID specified.
For the peer with the matching GUID. Immediately send back a “SynchronizationAction” with a snapshot of all application stores, and a timestamp indicating when this occurred. All other peers that receive this action that do not have the specified GUID, should simply disregard this request.
After the connecting peer broadcasts the received “SynchronizationAction” to all local stores, drop all queued Actions that occurred prior to the synchronization action and then begin broadcasting those as well.
At this point the app should be in a synchronized state and should be able to receive and broadcast actions freely. There are however, two major assumptions that need to be made in a peer-to-peer system such as this. First, that a store will completely finish processing an Action before receiving another in order to avoid dirty write states, and that the timestamps appended to each action have enough specificity to avoid collisions when they are being ordered and processed. In most applications this probably isn’t going to be a major issues, but for something like gaming or with an extremely high frequency of Actions, the importance of this grows larger.
There are many inherent benefits in using uni-directional data flow architectures when it comes to utilizing them in synchronous environments. Whether it is in a client-server model, or in a direct peer-to-peer setting this action based architecture is ideal for circumventing many of the tedious problems that come with layering synchronization on top of traditional MVC software architectures.
“Flux | Application Architecture for Building User Interfaces.” Flux | Application Architecture for Building User Interfaces. Web. 30 Mar. 2015. <http://facebook.github.io/flux/>.
“A Simple Library for Unidirectional Dataflow Architecture Inspired by ReactJS Flux.” Reflux. Web. 30 Mar. 2015. <https://github.com/spoike/refluxjs>.
“A Free, Open Project That Provides Browsers and Mobile Applications with Real-Time Communications (RTC) Capabilities via Simple APIs.” WebRTC. Web. 30 Mar. 2015. <http://www.webrtc.org/>.