mudb
Version:
Real-time database for multiplayer games
116 lines (73 loc) • 5.29 kB
Markdown
# MuRDA: Replicated Data Actions
MuRDA is another schema built on top of MuSchema which allows for replicated data actions across user defined documents. It is designed to support real time collaborative editing with optimistic execution and a localized undo buffer. It's still a work in progress so performance may not yet be optimal and APIs are all subject to change. These modules are all designed to be used along side the MuReplica modules which implement a simple client-server protocol for synchronizing MuRDA instances.
The basic idea in MuRDA is to turn all state transitions into a lattice of idempotent actions. To apply an action, a client first executes it optimistically locally and sends it to the server. The server then validates actions and broadcasts them to all clients in linearized order. Once any pair of clients have seen the same sequence of actions from the server their states are eventually synchronized, even if they diverge temporarily. Undo and redo functions are also built into MuRDA. These are implemented by storing a local undo buffer on each client. A client may play back their undo buffer to rewind various local actions.
## Example Usage
```
// TODO
```
## Technical details
Optimistic eventually-consistent replicated data structures
* Replicated data structure = A common data structure where all hosts in the system can see the same thing
* Optimistic = Clients can see the effects of their interactions immediately
* Eventually consistent = All clients eventually see the same thing
Each RDA defines 3 schemas:
* State: The state of the system, all the data, fields, etc. Think a "Model" in model view controller
* Actions: Discrete state transitions, usually generated by use inputs (ex update a field, increment a counter, etc.)
* Store: An internal data structure used to represent the state. May be significantly more complicated than the state itself.
Actions can be serialized and rebroadcast, stores and states are likewise serializable and pooled.
* Stores record a mutable data structure modeling the state
* Actions change the stores.
* State is an immutable copy/view of a store
## Action dispatch rules
Applying an action to a store is called "dispatch"-ing it.
When an action is dispatched to the store it is immediately applied to the store, mutating its state in place.
Actions form a semigroup under the dispatch/apply law, ie they're transitive and composable.
The actions must obey the move-to-front law:
g(f(g(x))) = g(f(x))
Consequence of move-to-front law: We get a simple protocol for applying actions in a way which achieves optimistic eventual consistency:
* Client 1: Apply action immediately and send to server
* Server: Take actions from clients, apply to store, rebroadcast to all clients
* Client 2: Receive action from server, apply immediately (do not rebroadcast)
## Action constructors
Constructing an action often requires some knowledge about the state of the store to initialize an action element.
Action constructors are called on the RDA given a reference to the store.
This can make a few things a little tricky, like we need to bind the store and pass it into each action instance recursively, etc.
## Undo/redo
We often need a way to locally undo/redo user actions.
To do this we augment the stores with the ability to calculate a pseudo-inverse, inv(S, a), for any action, $a$, relative to the current state of the store $S$.
This has the property that:
inv(S, a)(a(S)) = S
We can store the pseudo-inverse actions and their original action $A$ as pairs $inv(S, a), a$ in a stack which we can use to implement the undo/redo functions.
## State projections
Because stores often store a bunch of extra book keeping data, it's often easier for users to work with the encoded state
## Examples
### Registers
This is the most fundamental RDA.
All the RDA laws are built around generalizing and extending the behavior of the register.
Only one action type: Set register to value
Trivially obeys the move-to-front law
### Tuples
Can be some collection of registers.
Updating each register can be done independently.
Sets to either register commute, so still satisfies move-to-front rule.
### Maps
One step above registers are maps.
Have to use tombstones to handle deleting an element in a map.
Same basic idea.
Can be thought of as something like an infinite-tuple.
### Sets
Similar to maps, except each client has to generate a unique id for the key.
As along as each client generates a unique key, everything is cool.
### Lists
Similar to maps and list except the keys are generated as ordered elements and also unique.
To insert an element into the list we generate a random key in the interval between the two elements we are trying to insert into.
If we have unique client ids can do some stuff to simplify this a bit using the lower order bits to generate a range and break ties.
## Recursive composition of RDA's
We can recursively build RDAs out of other RDAs using the tools in the MuRDA. It comes with a few generic RDA types:
* Structs (aka tuple types)
* Lists
* Maps
Non-recursive RDAs:
* Constants
* Registers
All this stuff uses a bunch of gross template wizardry to provide a convenient user interface with nice typechecking properties.