Skip to main content

Map

The map represents a JSON like object, and it is the basis of the Ditto Document. Whenever you create a document you are creating a CRDT Map at its root. We nest maps within maps to allow complex JSON like document structures. A map is made up of properties and values. The values can be registers, counters, arrays, or maps.

do {    // Insert JSON-compatible data into Ditto    try ditto.store["foo"].upsert([        "boolean": true,        "string": "Hello World",        "number": 10,        "map": ["key": "value"],        "array": [1,2,3],        "null": nil    ])}catch {    //handle error    print(error)}

When do I use a map?

A map is useful when you want to make a list of items and update items over time within a document.

Updating a Map

Use keypath indexing to update and add keys to a map that is nested in another map.

collection.findByID("map_test").update(doc => {  doc.at("friends").set({     "beep": "boop"  })})
collection.findByID("map_test").update(doc => {  doc.at("friends.foo").set("bar")})

Remove

Values of map properties merge with the existing document, so simply omitting them from the CRDT map doesn't remove them. Instead, the CRDT map creates an operation for that key and existing keys remain unchanged.

This does not work to delete values; it is a no-op.

collection.findByID("map_test").update(doc => {  doc.at("friends").set({})})

This will adequately delete the given value using fine-grained updates which will be very efficient.

collection.findByID("map_test").update(doc => {  doc.at("friends.foo").remove()})

Concurrent updates

An update to any field in a map is sent independent of other changes within a map. For example, if you want to update the mileage every three seconds, we don't want to also replicate changes to all of the other keys in the map. Using the Map type allows you to make changes to just a single key in the Map efficiently.

Say that Devices A and B have this document:

{  "_id": "abc123",  "color": "red",  "make": "Toyota",  "mileage": 160000,  "inspections": "<very large map>"}

Device A calls upsert to change the property "color: red" to "color: blue".

upsert({  _id: "abc123",  color: "blue"})

Device B simultaneously updates the mileage.

findById("abc123").update(doc => {  doc.mileage.incrememt(200)})

When both devices synchronize, both updates will merge. You will see the mileage is incremented and the color is now blue.

{  "_id": "abc123",  "color": "blue",  "make": "Toyota",  "mileage": 160200,  "smogReports": "<very long json blob>"}

Update history

What would be the best approach when two devices make changes to the same field on the same document while both are offline, and then connect again. If you use a Register, the CRDT gives priority to the β€œlatest” change, because Registers are always Last Writer Wins. But if you would like to audit these conflicts, you can use a Map.

For example, say we want to render the inspections performed on a car in a list view. We provide a field inspections and give each writer an ID as the field. For example, say you have two peers: peer 1 has a unique ID of ghj789 and peer 2 has an ID def456.

You can model that list of operations as a map object inside of your document. The UI can then choose how to render these changes based on whatever information each peer has provided in the map.

{  "_id": "abc123",  "color": "red",  "make": "Toyota",  "mileage": 160000,  "inspections": {    "def456": {      "date": "2019-10-03",      "result": 1,      "mileage": 140000    } ,    "ghj789": {      "date": "2021-02-03",      "result": 1,      "mileage": 150000    }   }}

How it works

Ditto replicates deltas that describe changes to properties of the document. If a document has 100 named properties, and only the "X" property is changed, only the metadata and value for the change to the "X" property is replicated. If "X" is at the end of a path like "a.b.c.y.X = 'foo'" then the information that enables other replicas to correctly merge the nested objects that make up the path to "X" is replicated.

Maps merge with a add-wins semantic. This means if some property of the map is concurrently updated and removed, the add wins. The values of map properties merge using the correct method of their type.

Read more about how Ditto's CRDT works.

Concurrent types

One unique problem for Maps is that it is possible for one device to create a document where some property is a map, and another device creates the same document where that property is an array. For example:

Site A creates:

{  "name":"Bob Jones",  "address": {    "street":"Long Road",    "house number":10298,    "zip":"90210"  }}

Whilst Site B creates:

{  "name": "Bob Jones",  "address":[    10298,    "Long Road",    "90210"  ]}

In this instance we cannot merge an array with an object. We chose not to let the "last updated type" win, as this could lead to a ping-pong of type changes between the devices. Instead we keep BOTH values for the "address" property. We render only the last updated type when we show JSON, but we also provide a way for the programmer to chose which type to render for a property, or to render ALL types for a property. This way we can manage type level conflict, and allow different versions of an application that use different implicit schema to co-exist.

Read more about how to manage schema changes.

New and Improved Docs

Ditto has a new documentation site at https://docs.ditto.live. This legacy site is preserved for historical reference, but its content is not guaranteed to be up to date or accurate.