Observing Changes

Overview

The most important API, which is fundamental to working with Ditto, is the ability to observe data changes that occur in the store. This allows your app to be reactive, simplifying your architecture, and abstracting the timing complexity of data synchronization that is occurring in the background. Instead of performing, point-in-time queries, you app can simply register a query to observe, which is called a "live query", and you will receive callbacks whenever data changes related to it.

This allows you to decouple actions in your applications with UI updates. You can bind your UI elements to live queries and then simply perform writes to Ditto from your actions elsewhere in the app. The live query will fire in response to those other actions and whenever data is received from other devices as well.

The example below creates a live query through the API observe(). This API combines two different actions related to observing changes. First, it registers an observer which will fire the callback whenever any data changes related to this query in the local store. Second, it also creates a subscription for data from other devices based off the query. For simple applications, using this combined API is easier than writing the query twice for each action. However, for more complex applications, where you might want to subscribe to a larger query of data from other devices whenever the app is running, but then have specific live queries for subsets of the data tied to certain views, you can separate the two actions, as described further down.

// --- Action somewhere in your application
func userDidInsertCar() {
    _ = ditto.store.collection("cars").insert([
        "model": "Ford",
        "color": "black"
    ])
}

// Register live query to update UI
let liveQuery = ditto.store.collection("cars").find("color == 'red'").observe { cars, event in
    switch event {
    case .initial:
        self.dataSource = cars
        self.tableView.reloadData()
    case .update(let updates):
        self.tableView.beginUpdates()
        self.tableView.performBatchUpdates({
            let deletionIndexPaths = updates.deletions.map { idx -> IndexPath in
                return IndexPath(row: idx, section: 0)
            }
            self.tableView.deleteRows(at: deletionIndexPaths, with: .automatic)
            let insertionIndexPaths = updates.insertions.map { idx -> IndexPath in
                return IndexPath(row: idx, section: 0)
            }
            self.tableView.insertRows(at: insertionIndexPaths, with: .automatic)
            let updateIndexPaths = updates.updates.map { idx -> IndexPath in
                return IndexPath(row: idx, section: 0)
            }
            self.tableView.reloadRows(at: updateIndexPaths, with: .automatic)
            for move in updates.moves {
                let from = IndexPath(row: move.from, section: 0)
                let to = IndexPath(row: move.to, section: 0)
                self.tableView.moveRow(at: from, to: to)
            }
        })
        self.dataSource = cars
        self.tableView.endUpdates()
    default: break
    }
}
// --- Action somewhere in your application
-(void) userDidInsertCar() {
    [[ditto.store collection:@"cars"] insert:@{
        @"model": @"Ford",
        @"color": @"black"
    }];
}

// Register live query to update UI
DITLiveQuery *liveQuery = [[collection find:@"color == 'red'"]
    observe:^(NSArray<DITDocument *> *docs, DITLiveQueryEvent *event) {
        if (event.isInitial) {
            self.dataSource = cars;
            [self.tableView reloadData];
        } else {
            [self.tableView beginUpdates];
            [self.tableView performBatchUpdates:^{
                NSArray *deletionIdxs = [self mapIndexesToIndexPaths: event.deletions];
                [self.tableView deleteRowsAtIndexPaths:deletionIdxs withRowAnimation:UITableViewRowAnimationAutomatic];
                NSArray *insertionIdxs = [self mapIndexesToIndexPaths: event.insertions];
                [self.tableView insertRowsAtIndexPaths:deletionIdxs withRowAnimation:UITableViewRowAnimationAutomatic];
                NSArray *updateIdxs = [self mapIndexesToIndexPaths: event.updates];
                [self.tableView reloadRowsAtIndexPaths: withRowAnimation:UITableViewRowAnimationAutomatic];

                [event.moves enumerateObjectsUsingBlock:^(DITLiveQueryMove *move, NSUInteger idx, BOOL *stop) {
                    NSIndexPath *from = [NSIndexPath indexPathForRow:move.from.intValue inSection:0];
                    NSIndexPath *to = [NSIndexPath indexPathForRow:move.to.intValue inSection:0];
                    [self.tableView moveRowAtIndexPath:from toIndexPath:to];
                }];
            } completion:^(BOOL finished) {}];
            self.dataSource = cars;
            [self.tableView endUpdates];
        }
}];

- (NSArray<NSIndexPath *>) mapIndexesToIndexPaths(NSArray<NSNumber *> source) {
    NSMutableArray *mapped = [NSMutableArray arrayWithCapacity:[source count]];
    [source enumerateObjectsUsingBlock:^(NSNumber *i, NSUInteger idx, BOOL *stop) {
        NSIndexPath *ip = [NSIndexPath indexPathForRow:i.intValue inSection:0];
        [mapped addObject:ip];
    }];
    return mapped;
}
// --- Action somewhere in your application
fun userDidInsertCar() {
    ditto.store.collection("cars").insert(mapOf(
        "model" to "Ford",
        "color" to "black"
    ))
}

// Register live query to update UI
this.liveQuery = ditto.store.collection("cars").
    .findAll()
    .observe { docs, event ->
        when (event) {
            is DittoLiveQueryEvent.Update -> {
                runOnUiThread {
                    this.adapter.set(docs)
                    this.adapter.insert(event.insertions)
                    this.adapter.delete(event.deletions)
                    this.adapter.update(event.updates)
                }
            }
            is DittoLiveQueryEvent.Initial -> {
                runOnUiThread {
                    this.adapter.addCars(docs)
                }
            }
        }
    }
// --- Action somewhere in your application
public void userDidInsertCar() {
    Map<String, Object> content = new HashMap<>();
    content.put("model", "Ford");
    content.put("color", "black");
    ditto.store.collection("cars").insert(content);
}

// Register live query to update UI
this.liveQuery = ditto.store.collection("cars").
    .findAll()
    .observe((docs, event) -> {
        if (event instanceof DittoLiveQueryEvent.Update) {
            DittoLiveQueryEvent.Update updateEvent = (DittoLiveQueryEvent.Update) event;
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    this.adapter.set(docs);
                    this.adapter.insert(updateEvent.insertions);
                    this.adapter.delete(updateEvent.deletions);
                    this.adapter.update(updateEvent.updates);
                }
            });
        } else if (event instanceof DittoLiveQueryEvent.Initial) {
            DittoLiveQueryEvent.Initial initialEvent = (DittoLiveQueryEvent.Initial) event;
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    this.adapter.addCars(docs);
                }
            });
        }
    });
// --- Action somewhere in your application
void user_did_insert_car()
{
    var carsDocument = new Dictionary<string, object>
    {
        { "model", "Ford" },
        { "color", "black" }
    };
    ditto.Store.Collection("cars").Insert(carsDocument);
}

// Register to observe and create a subscription
var liveQuery = ditto.Store.Collection("cars").Find("color == 'red'").Observe((docs, DittoLiveQueryEvent) =>
{
    // Do something
});
// --- Action somewhere in your application
void user_did_insert_car() {
    ditto.store.collection("cars").insert({
        {"model", "Ford"},
        {"color", "black"}
    });
}

// --- Register live query to update UI
std::shared_ptr<LiveQuery> query = collection
  .find("color == 'red'")
  .observe(LiveQueryEventHandler{
    [&](std::vector<Document> docs, LiveQueryEvent event) {
      // Handle updating UI using event properties
      if (event.is_initial) {
        // ...
      } else {
        // ...
      }
    }});
const liveQuery = ditto.store.collection('cars').find("color == 'red'").observe((cars, event) => {
    if (event.isInitial) {
        // There is only one initial event at the beginning receiving all
        // documents currently in the store matching the query.
        //
        // Initialize your UI with it:
        console.log('Live query started with initial set of red cars:')
        console.log(cars)
    } else {
        // All subsequent event's are update events. As for the initial event,
        // we receive all documents currently matching the query. The event
        // itself carries the previous array of documents plus information
        // on which of the documents have been inserted, deleted, updated, or
        // moved.
        //
        // Update your UI with it:
        for (const index of event.deletions) {
            console.log(`Car deleted at index (relative to the last documents array): ${index}:`)
            console.log(event.oldDocuments[index])
        }

        for (const index of event.insertions) {
            console.log(`Car inserted at index: ${index}:`)
            console.log(cars[index])
        }

        for (const index of event.updates) {
            console.log(`Car updated at index: ${index}:`)
            console.log(cars[index])
        }

        for (const move of event.moves) {
            console.log(`Car moved from index: ${move.from}, to index: ${move.to}`)
            console.log(cars[move.to])
        }
    }
})

Observations

To monitor for local data changes in the Ditto store, you can observe queries using observeLocal. This is called a "live query". The data returned in the query applies only to the data held locally in the Ditto store.

// --- Action somewhere in your application
func userDidInsertCar() {
    ditto.store["cars"].insert([
        "model": "Ford",
        "color": "black"
    ])
}

// Register live query to update UI
let liveQuery = ditto.store["cars"]
    .find("color == 'red'")
    .observeLocal { cars, event in
        // Do something...
    }
// --- Action somewhere in your application
-(void) userDidInsertCar() {
    [[ditto.store collection:@"cars"] insert:@{
        @"model": @"Ford",
        @"color": @"black"
    }];
}

// Register live query to update UI
DITLiveQuery *liveQuery = [[collection find:@"color == 'red'"]
  observeLocal:^(NSArray<DITDocument *> *docs, DITLiveQueryEvent *event) {
    // Do something
}];
// --- Action somewhere in your application
fun userDidInsertCar() {
    ditto.store.collection("cars").insert(mapOf(
        "model" to "Ford",
        "color" to "black"
    ))
}

// --- Register live query to update UI
this.liveQuery = ditto.store.collection("cars").
    .findAll()
    .observe { docs, event ->
    // Do something...
}
// --- Action somewhere in your application
public void userDidInsertCar() {
    Map<String, Object> content = new HashMap<>();
    content.put("model", "Ford");
    content.put("color", "black");
    ditto.store.collection("cars").insert(content);
}

// --- Register live query to update UI
this.liveQuery = ditto.store.collection("cars")
    .findAll()
    .observe((docs, event) -> {
        // Do something...
    });
// --- Action somewhere in your application
void user_did_insert_car()
{
    var carsDocument = new Dictionary<string, object>
    {
        { "model", "Ford" },
        { "color", "black" }
    };
    ditto.Store.Collection("cars").Insert(carsDocument);
}

// --- Register live query to update UI
var localLiveQuery = ditto.Store.Collection("cars").Find("color == 'red'").ObserveLocal((docs, DittoLiveQueryEvent) =>
{
    // Do something...
});
// --- Action somewhere in your application
void user_did_insert_car() {
    ditto.tore.collection("cars").insert({
        {"model", "Ford"},
        {"color", "black"}
    });
}

// --- Register live query to update UI
std::shared_ptr<LiveQuery> query = collection
  .find("color == 'red'")
  .observe_local(LiveQueryEventHandler{
    [&](std::vector<Document> docs, LiveQueryEvent event) {
      // Handle updating UI using event properties
      if (event.is_initial) {
        // ...
      } else {
        // ...
      }
    }});
const liveQuery = ditto.store.collection('cars')
    .find("color == 'red'")
    .observeLocal((cars, event) => {
        // Do something...
    })

Subscriptions

Ditto's synchronization system is query-based, which means, that by default the SDK will not sync data with other devices. Instead, the app creates query-based subscriptions that define which data it wants to sync. When the device is subscribed to a query, then other devices will share data matching that query with it:

Syncing and Live Queries 1

Given that Ditto works peer-to-peer, devices can form into arbitrary groups based on the proximity to one another, or rather they create an ad-hoc mesh network. Ditto's synchronization system allows for devices to share data through another device, called "multi-hop" sync. The only requirement for this to occur is that all devices in the chain must be subscribed to the same data, as shown below:

Syncing and Live Queries 2

To create subscriptions is similar to, or can also be combined with, observations (as described above):

// Register a subscription
let subscription = ditto.store.collection("cars")
    .find("color == 'red'")
    .subscribe()

// Register to observe and create a subscription
let liveQuery = ditto.store.collection("cars")
    .find("color == 'red'")
    .observe { cars, event in
        // Do something
    }
// Register live query to update UI
DITSubscription *subscription = [[collection find:@"color == 'red'"] subscribe];

// Register to observe and create a subscription
DITLiveQuery *liveQuery = [[collection find:@"color == 'red'"]
  observe:^(NSArray<DITDocument *> *docs, DITLiveQueryEvent *event) {
    // Do something
}];
// Register a subscription
this.subscription = ditto.store.collection("cars").
    .findAll()
    .subscribe()

// Register to observe and create a subscription
this.liveQuery = ditto.store.collection("cars").
    .findAll()
    .observe { docs, event ->
        // Do something...
    }
// Register a subscription
this.subscription = ditto.store.collection("cars").
    .findAll()
    .subscribe();

// Register to observe and create a subscription
this.liveQuery = ditto.store.collection("cars").
    .findAll()
    .observe((docs, event) -> {
        // Do something...
    });
// Register a subscription
var subscription = ditto.Store.Collection("cars").Find("color == 'red'").Subscribe();

// Register to observe and create a subscription
var liveQuery = ditto.Store.Collection("cars").Find("color == 'red'").Observe((docs, DittoLiveQueryEvent) =>
{
    // Do something
});
// Register a subscription
Subscription subscription = ditto.store.collection("cars").find("color == 'red'").subscribe();

// Register to observe and create a subscription
std::shared_ptr<LiveQuery> query = collection
  .find("color == 'red'")
  .observe_local(LiveQueryEventHandler{
    [&](std::vector<Document> docs, LiveQueryEvent event) {
      // Handle updating UI using event properties
      if (event.is_initial) {
        // ...
      } else {
        // ...
      }
    }});
// Register a subscription
const subscription = ditto.store.collection('cars')
    .find("color == 'red'")
    .subscribe()

// Register to observe and create a subscription
const liveQuery = ditto.store.collection('cars')
    .find("color == 'red'")
    .observe((cars, event) => {
        // Do something
    })

Top