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.

// Insert JSON-compatible data into Dittoawait ditto.store.collection('people').upsert({  boolean: true,  string: 'Hello World',  number: 10,  map: { key: 'value' },  array: [],  null: null,})

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.

A list of objects

For example, we want to render the inspections that were performed on a car in a list view. You can model that list as a map object inside of your document.

{  "_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    }   }}

Maps are queriable using the find API as part of the Ditto query language.

Updating fields concurrently

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>"}

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 remove-wins semantic. This means if some property of the map is concurrently updated and removed, the remove wins. The values of map properties merge using the correct method of their type.

Read more about how Ditto's CRDT works.

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.