Arcentry Architecture: State
This guide is targeted towards developers working with Arcentry's code-base. If you are an end-user, please consult the REST-API and Api-Docs documentation instead. If you are an enterprise customer, please find information in the Enterprise section of the documentation.
Persistence in Arcentry has several challenging requirements:
- Any change to a diagram needs to be instantly visible to the user to make sure the app feels responsive.
- There's no save button - instead, changes are continuously sent to the backend. This means that only the smallest possible amount of data for every change needs to be transmitted, and multiple changes need to be batched.
- Arcentry allows for realtime user collaboration a la Google Docs. Any user can make any change to a diagram at any time, and these changes need to be reflected for all other users simultaneously.
- Changes to a diagram can also be made programmatically via the API. These changes too need to be visible to all users currently looking at the given diagram.
- A local history of changes needs to be kept to allow for undo/redo operations.
The solution to all this is an approach called Operational Transformation, of which Arcentry implements a somewhat naive approach.
In a nutshell, there is a central state, a list of objects, and meta-data that describes a single document in Arcentry. This state cannot be manipulated directly. Instead, manipulations happen by submitting a transaction object, containing instructions to CREATE, UPDATE, SET, or DELETE an object to a processing function. This processing function merges the update into the current state, saves it, and notifies all interested listeners.
Everything in Arcentry works this way. When you move a component around the canvas, you don't actually interact with the component's 3D object directly. Instead, the component class sends a stream of UPDATE transformations to the state, which - once applied - notify the component to apply these changes, e.g. by changing the 3D model's position.
This sounds a bit complicated, but it's genuinely an elegant way to decouple user input and server side changes from the state of a diagram. Let's dive a bit deeper into how that works:
What does the State look like?
The state is expressed as a key-value map. The keys are object-ids, unique to a given document. The values are blobs of data that describe an object. This is true for actual things you can see on a diagram, e.g. components, line groups, areas etc., as well as abstract concepts, e.g. the layers in a document. Keys that specify abstract concepts are prefixed with meta-.
Here's an example of state data, describing a diagram with a single Object and a line connecting to it.
{
"1e1dtgtpf-rdmrp6si0": {
"data": {
"icon": "cogs",
"opacity": 1,
"position": { "x": 0.5, "y": 0 },
"rotation": 4.71238898038469,
"iconColor": "#606060",
"imagePath": null,
"componentId": "generic.generic-batch-processor",
"primaryColor": "#9F9F9F",
"backgroundColor": "#FFFFFF"
},
"type": "component"
},
"1e1dtk8ub-b9necsv8p": {
"data": {
"lines": [ [ 0, 1 ] ],
"anchors": [
{ "x": 0, "y": 0.5, "id": "1e1dtgtpf-rdmrp6si0", "type": 1, "index": 6 },
{ "x": -3, "y": 0.5, "id": "1e1dso844-2a5kpp7h08", "type": 1, "index": 7 }
],
"lineDash": 1,
"lineWidth": 0.1,
"strokeStyle": "#E61898",
"arrowAnchorIndices": { "0": false, "1": true }
},
"type": "line-group",
}
}
How is the state manipulated?
By sending a transaction object. Each transaction object has at least a type and an object id. Type can be one of:
- CREATE creates an object
- UPDATE merges a set of changes into an object's data
- SET overwrites all data of an object with a new set of data
- DELETE removes an object
Here are some examples:
// Create a new PostgreSQL component
{
action: "create",
type: "component", //A type is required when creating an object
id: "1e1h0nls1-891rd8f0b",
data: {
componentId: "database.postgres",
position: {x: -10.5, y: -9},
rotation: 0,
opacity: 1
}
}
// Moving the same object to a new position
{
id: "1e1h0nls1-891rd8f0b",
action: "update",
data: {
position: {x: -17.5, y: 0}
}
}
// Deleting the object
{
action: "delete"
id: "1e1h0nls1-891rd8f0b"
}
State is not only shared between browser and server, but also forwarded by the server to other subscribed clients and even shared by the database across multiple connected servers. Here's how this all fits together: