Tutorial: Subscribe to changes

Serverless DatabaseData Structure Subscribe to changes

A very common use-case in Webcom applications is to receive notifications as soon as some data is updated (either by the running client itself or by any other client). This strength of the Webcom back end relies on the real-time feature of its underlying database.

In practice, the idea is to subscribe to a given type of update event on a given data node (or subscriber node). From then on, the callback function registered at subscription will be called for each matching update event on the data at this node. In addition to the data itself, the notified event may contain useful pieces of information such as the acknowledgement status of the data.

More experimented developers may go further and read also Subscribable events in depth.

Subscribable event types

Webcom currently provides 6 types of update events you can subscribe to, split into 2 groups:

Type of update event Description
Value Change The value of the subscriber node has been updated.
The raised event includes the new value and its acknowledgement status.
Value Acknowledgement The value of the subscriber node has been acknowledged by the back end.
The raised event includes the value of the node.

Child Addition

A new child has been added to the subscriber node.
The raised event includes the key of the new child, its value, the key of the preceding child (following the Webcom key ordering), and the acknowledgement status of this child addition.
Child Change The value of an existing child of the subscriber node has been updated.
The raised event includes the key of the updated node, its value, the key of the preceding child (following the Webcom key ordering), and the acknowledgement status of this child update.
Child Removal A child of the subscriber node has been removed.
The raised event includes the key of the removed child, the value of the child before its removal, and the acknowledgement status of this child removal.
Child Acknowledgement The value of a child of the subscriber node has been acknowledged by the back end (including child removals).
The raised event includes the key of the acknowledged child and its value (or null for removed children).

Within each group, several types of update events may be subscribed simultaneously. For example, Child Addition and Child Removal may be subscribed at the same time, however Child Addition and Value Change cannot. Once a subscription registered, the associated callback function will be called each time an event of the type that has been subscribed is raised. The details for each event type are given in the following sections.

In addition, the callback function may also be notified of the special revocation event. The revocation event is raised by the Webcom back end as soon as the security rules prevent the subscriber node from being read, or the size of the subscriber node becomes too large. Once raised, the revocation event is passed to the callback function, and the associated subscription is automatically canceled, ie the callback will never be called any longer from then on.

Subscribing and unsubscribing

In JavaScript, subscribing relies on the on(...) method. The first parameter expects one or more subscribed types of event, while the second one expects the notification callback. The optional third parameters expects a callback for revocation events.

The notified events passed to the callback are instances of the DataSnapshot class: among the methods common to all subscribed types of events, acknowledged() indicates whether the data update represented by the event is acknowledged by the back end or local to the client cache, and the eventType() method returns the type of the notified event (it is useful when the callback is subscribed to several types of event at a time) (replace “<your-app>” with your actual application identifier):

const ref = new Webcom("<your-app>");
let node = ref.child("contacts");
node.on(["value", "value_ack"], // subscribed types of update events
    datasnapshot => { // the callback to call when an event is raised (except revocations)
        if (datasnapshot.eventType() === "value") {
            console.log("The data at CONTACTS has changed:", JSON.stringify(datasnapshot.val()));
        }
        console.log("The data at CONTACTS", datasnapshot.acknowledged() ? "is" : "is not yet", "acknowledged");
    },
    revocationError => { // optional callback for revocation events
        console.log("The subscription on CONTACTS has been revoked!", revocationError.message);
    });

Unsubscribing uses the off(...) method:

node.off(); // unsubscibe all callbacks on the node
node.off(["value", "value_ack"]); // unsubscribe all callbaks subscribed to "value" or "value_ack" on the node

In Kotlin, a subscription is registered using the subscribe(...) method of DatasyncNode objects, and is managed by the DatasyncManager object that has created this DatasyncNode instance. The first argument of this method represents the subscribed types of event, excluding “Value Acknowledgement” and “Child Acknowledgement”, which are separately requested using the boolean includesAcknowledgements parameter. Its last argument is a callback function, which is called for each raised event, including revocation events.

The notified events passed to the callback are instances of a subclass of the Event class that represents their actual type. Among the properties common to all of these subclasses, dataAcknowledged indicates whether the data update represented by the event is acknowledged by the back end or local to the client cache, and isAcknowledgement indicates whether the event is one of “Value Acknowledgement” or “Child Acknowledgement”:

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val node = manager / "contacts"
val subscription = node.subscribe(ValueChanged.WithData, includesAcknowledgements = true) {
  when (it) {
    is WebcomResult.Succes -> {
      if (!it.result.isAcknowledgement) println("The data at CONTACTS has changed: ${it.result.value}")
      println("The data at CONTACTS ${if (it.result.dataAcknowledged) "is" else "is not"} acknowledged")
    }
    is WebcomResult.Failre ->
      println("The subscription on CONTACTS has been revoked! ${it.failure.message}")
  }
}

Unsubscribing is done using the cancel() method of the Subscription object returned by the previous subscribe(...) method:

subscription.cancel()

Note that subscriptions are managed by DatasyncManager objects so that each manager automatically cancels its registered subscription when released (ie garbage-collected). A typical use-case is to create a dedicated manager for each Activity or Fragment of your Android app and bind their life-cycles. Thus, this avoids managing explicitly the cancelation of subscriptions, which is generally a tedious task.

In Swift, a subscription is registered using the subscribe() method of DatasyncNode objects and is managed by the DatasyncManager object that has created this DatasyncNode instance. A subscription is specified by a callback function that will be called each time the corresponding event is raised. The subscribed type of event is represented by a value of the dedicated DatasyncEventType enum.

The subscription callback receives as parameter a DatasyncEvent value that provides convenient methods to retrieve the kind of the just raised event, the value of the associated data node, as well as the corresponding DatasyncSubscription object (which makes it possible to individually revoke the subscription without waiting for the manager to do it).

let manager = Webcom.defaultApplication.datasyncService.createManager()
let contactsNode = manager / "contacts"
contactsNode?.subscribe(to: .valueChange) { event in
    guard let allContacts: [Contact] = event.value.decoded() else {
        return
    }
    print("The 'contacts' node has \(allContacts.count) child nodes")
}

Subscriptions are managed by DatasyncManager objects so that each manager automatically cancels its registered subscription when released. The idea is to create a manager for each view controller and bind their life-cycles, in order to avoid explicitly managing the cancelation of subscriptions, which is generally a tedious task.

This functionality is not available on the Webcom REST API.

Value events

Value events are raised as soon as the value of the subscriber node, considered as a whole, changes. Considering the value of a nonexistent data node is null, these events are also raised when the subscriber node is deleted (ie its value becomes null).

Value Change” event

This event is raised:

  • Initially at subscription registration, with the current known value of the subscriber node,
  • Then each time the value of the subscriber node changes, with its new value.

This event also conveys whether the value of the subscriber node has been acknowledged by the Webcom back end or only updated locally within the client cache.

In JavaScript, the “Value Change” event type to subscribe to is represented by the "value" string. The val() method of the DataSnapshot object passed to the callback gets the subscriber node value.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Display the count of contacts in real time
contactsNode.on("value",
    snapshot => console.log("There are now", Object.keys(snapshot.val()).length, "contacts"),
    revocationError => console.log("Contact watching revoked:", revocationError.message)
);

In Kotlin, there are 2 variants of the “Value Change” event type:

  • the ValueChanged.WithData object, which raises events of the ValueChangedWithDataEvent class,
  • the ValueChanged.WithoutData object, which raises events of the ValueChangedWithoutDataEvent class that provides no property to get the value of the subscriber node. This particular event type is used when you don't need to get the subscriber node value.
val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Display the count of contacts in real time
val subscription = contactsNode.subscribe(ValueChanged.WithData) {
  when (it) {
    is WebcomResult.Success -> println("There are now ${it.result.value.convertedAsMap<Contact>()?.size} contacts")
    is WebcomResult.Failure -> println("Contact watching revoked: ${it.error.message}")
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Display the count of contacts in real time
contactsNode?.subscribe(to: .valueChange) { event in
    print("There are now \(event.value.decoded(as: [String: Contact].self)?.count ?? 0) contacts")
} onCompletion: { completion in
    if case let .revocation(reason: reason) = completion {
        print("Contact watching revoked: \(reason)")
    }
}

Value Acknowledgement” event

This event:

  • Is never raised initially at subscription registration,
  • Is raised following a “Value Change” event, whose conveyed value of the subscriber node is NOT acknowledged by the Webcom back end:
    • as soon as the value of the subscriber node is acknowledged by the back end,
    • AND its value is the same as the one conveyed by the previous “Value Change” event (otherwise a new “Value Change” event is raised).

For convenience, it conveys the acknowledged value of the subscriber node, which is necessarily equal to the value of the previous “Value Change” event, and the acknowledgement status of the value, which is necessarily true.

In JavaScript, the “Value Acknowledgement” event type to subscribe to is represented by the "value_ack" string.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Display a message each time the contact list is acknowledged by the back end
contactsNode.on(["value", "value_ack"], snapshot => {
  if (snapshot.acknowledged()) { console.log("The contact list is acknowledged"); }
});

As explained above, in Kotlin, there is no explicit objet to represent the “Value Acknowledgement” event type. Instead, you must subscribe to the ValueChanged.WithData (or ValueChanged.WithoutData) event and pass true to the includesAcknowledgement parameter of the subscribe method.

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Display a message each time the contact list is acknowledged by the back end
contactsNode.subscribe(ValueChanged.WithoutData, includesAcknowledgement = true) {
    if (it is WebcomResult.Success && it.dataAcknowledged) println("The contact list is acknowledged")
}

As a consequence, contrary to the JavaScript SDK, it is not possible to subscribe to the “Value Acknowledgement” event alone, without subscribing to the “Value Change” event. It is not really a limitation, since this use case is quite useless (see the tip corner below).

let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Display a message each time the contact list is acknowledged by the back end
contactsNode?.subscribe(to: .valueEvent) {
    if $0.value.isAcknowledged {
        print("The contact list is acknowledged")
    }
}

Note that, in practice, in order to watch the acknowledgement of the value of a data node, it is necessary to subscribe to BOTH:

  • Value Change” event, in case the notified value is already acknowledged (typically the value has been updated by another client),
  • Value Acknowledgement” event, in case the value is first updated locally on the client (raising the previous event in not acknowledged state) and then acknowledged by the back end.

Child events

Child events are raised as soon as a child of the subscriber node is updated. When watching children, it is possible to distinguish between additions, changes (ie updates of existing children) and removals of children. Such raised events additionally convey, with respect to previous Value events, the key of the concerned child and sometimes the key of the child that precedes the concerned child along the Webcom key ordering.

Child Addition” event

This event is raised:

  • Initially at subscription registration, one time for each of the current known children of the subscriber node,
  • Then each time a child node is added to the subscriber node, with the key and the value of this child.

This event also conveys the key of the child of the subscriber node that directly precedes (along the Webcom key ordering) the added child, as well as a boolean indicating whether the added child has been acknowledged by the Webcom back end or only updated locally within the client cache.

In JavaScript, the “Child Addition” event type to subscribe to is represented by the "child_added" string. The first argument passed to the callback is a DataSnapshot object, whose name() method gives the key of the added child, and the val() method gets its value. The second argument passed to the callback is a string representing the key of the child that precedes the added child or null if it is the 1st child of the subscriber node.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a new contact is added
contactsNode.on("child_added",
        (snapshot, leftChild) => myView.addItem({name: snapshot.name(), value: snapshot.val(), after: leftChild}),
        revocationError => console.log("Contact watching revoked:", revocationError.message)
);

In Kotlin, the “Child Addition” event type is represented by the Child.Addition object, which in turn raises events of the ChildEvent class. As this class is also used to raise events associated with other child event types, it provides the trigger property to retrieve the actual raised event. The node property refers to the data node of the added child and makes it possible to get its key. The value property gives the value of the added child.

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
val subscription = contactsNode.subscribe(Child.Addition) {
  when (it) {
    is WebcomResult.Success -> myView.addItem(name = it.result.node.key, value = it.result.value.convertedAs<Contact>())
    is WebcomResult.Failure -> println("Contact watching revoked: ${it.error.message}")
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
contactsNode?.subscribe(to: .childAddition) { event in
    myView.addItem(name: event.key, value: event.value.decoded())
} onCompletion: { completion in
    if case let .revocation(reason: reason) = completion {
        print("Contact watching revoked: \(reason)")
    }
}

Child Change” event

This event:

  • Is never raised initially at subscription registration,
  • Is raised each time the value of an existing child node of the subscriber node changes (except when the new value of the child is null, in this case a Child Removal event is raised), with the new value of this child.

This event also conveys the key of the child of the subscriber node that directly precedes (along the Webcom key ordering) the changed child, as well as a boolean indicating whether the new value of the child has been acknowledged by the Webcom back end or only updated locally within the client cache.

In JavaScript, the “Child Change” event type to subscribe to is represented by the "child_changed" string. The first argument passed to the callback is a DataSnapshot object, whose name() method gives the key of the changed child, and the val() method gets its new value. The second argument passed to the callback is a string representing the key of the child that precedes the changed child or null if it is the 1st child of the subscriber node.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a contact is updated
contactsNode.on("child_changed",
        (snapshot, leftChild) => myView.updateItem({name: snapshot.name(), value: snapshot.val(), after: leftChild}),
        revocationError => console.log("Contact watching revoked:", revocationError.message)
);

In Kotlin, the “Child Change” event type is represented by the Child.Change object, which in turn raises events of the ChildEvent class. Similarly to the processing of “Child Addition” events, this class provides the trigger property to retrieve the actual raised event. The node property refers to the data node of the changed child and makes it possible to get its key. The value property gives the new value of the changed child.

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a contact is updated
val subscription = contactsNode.subscribe(Child.Change) {
  when (it) {
    is WebcomResult.Success -> myView.updateItem(name = it.result.node.key, value = it.result.value.convertedAs<Contact>())
    is WebcomResult.Failure -> println("Contact watching revoked: ${it.error.message}")
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
contactsNode?.subscribe(to: .childChange) { event in
    myView.updateItem(name: event.key, value: event.value.decoded())
} onCompletion: { completion in
    if case let .revocation(reason: reason) = completion {
        print("Contact watching revoked: \(reason)")
    }
}

Child Removal” event

This event:

  • Is never raised initially at subscription registration,
  • Is raised each time an existing child node of the subscriber node is removed (or its value is updated to null), with the old value of this child.

This event also conveys whether the removal of the child has been acknowledged by the Webcom back end or only updated locally within the client cache.

In JavaScript, the “Child Removal” event type to subscribe to is represented by the "child_removed" string. The argument passed to the callback is a DataSnapshot object, whose name() method gives the key of the changed child, and the val() method gets its deleted value.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a contact is deleted
contactsNode.on("child_removed",
        snapshot => myView.deleteItem({name: snapshot.name()}),
        revocationError => console.log("Contact watching revoked:", revocationError.message)
);

In Kotlin, the “Child Removal” event type is represented by the Child.Removal object, which in turn raises events of the ChildEvent class. Similarly to the processing of “Child Addition” and “Child Change” events, this class provides the trigger property to retrieve the actual raised event. The node property refers to the data node of the removed child and makes it possible to get its key. The value property gives the old value of the deleted child.

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a contact is deleted
val subscription = contactsNode.subscribe(Child.Removal) {
  when (it) {
    is WebcomResult.Success -> myView.deleteItem(name = it.result.node.key)
    is WebcomResult.Failure -> println("Contact watching revoked: ${it.error.message}")
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
contactsNode?.subscribe(to: .childRemoval) { event in
    myView.deleteItem(name: event.key)
} onCompletion: { completion in
    if case let .revocation(reason: reason) = completion {
        print("Contact watching revoked: \(reason)")
    }
}

Child Acknowledgement” event

This event:

  • Is never raised initially at subscription registration,
  • Is necessarily raised following a “Child Addition”, “Child Change” or “Child Removal” event, whose associated child value is NOT acknowledged by the Webcom back end:
    • as soon as the value of the subscriber node child is acknowledged by the back end,
    • AND its value is the same as the one conveyed by the previous “Child ...” event (otherwise a new “Child ...” event is raised).

For convenience, it conveys the acknowledged value of the subscriber node child, which is necessarily equal to the value of the previous “Child ...” event (null for a removed child), and the acknowledgement status of the value, which is necessarily true.

WARNING: when subscribed to the “Child Acknowledgement” event, you receive all child acknowledgements, even if it follows a child event that is not subscribed. For example, if you subscribe to “Child Addition” and “Child Acknowledgement” events, you may receive a “Child Acknowledgement” event that doesn't necessarily follow the addition of a child to the subscriber node, you will also receive acknowledgements for changes and removal of child nodes.

In practice, a callback often subscribes to all of the child events. Let's wrap up all together in our contact book example:

In JavaScript, the “Child Acknowledgement” event type to subscribe to is represented by the "child_ack" string.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time the list of contacts is updated
contactsNode.on(["child_added", "child_changed", "child_removed", "child_ack"],
        (snapshot, leftChild) => {
          switch (snapshot.eventType()) {
            case "child_added":
              myView.addItem({name: snapshot.name(), value: snapshot.val(), after: leftChild});
              break;
            case "child_changed":
              myView.updateItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_removed":
              myView.deleteItem({name: snapshot.name()}); // mark the item removed without deleting it
              break;
            default: // "child_ack" events do nothing here
              break;
          }
          // darken, lighten or delete the item depending on its acknowledged and removed statuses
          myView.ackItem({name: snapshot.name(), acknowledged: snapshot.acknowledged()});
        },
        revocationError => console.log("Contact watching revoked:", revocationError.message)
);

Like with “Value Acknowledgement” event type, in Kotlin, there is no explicit objet to represent the “Child Acknowledgement” event type. Instead, you must subscribe to a Child event and pass true to the includesAcknowledgement parameter of the subscribe method.

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
val subscription = contactsNode.subscribe(Child.Addition + Child.Change + Child.Removal, includesAcknowledgement = true) {
  when (it) {
    is WebcomResult.Success -> {
      when (it.result.trigger) {
        ADDED -> myView.addItem(name = it.result.node.key, value = it.result.value)
        CHANGED -> myView.updateItem(name = it.result.node.key, value = it.result.value)
        REMOVED -> myView.deleteItem(name = it.result.node.key) // mark the item removed without deleting it
        ACKNOWLEDGED -> {} // ACKNOWLEDGED triggered events do nothing here
      }
      // darken, lighten or delete the item depending on its aknowledged and removed statuses
      myView.ackItem(name = it.result.node.key, acknowledged = it.result.dataAcknowledged) 
    }
    is WebcomResult.Failure -> println("Contact watching revoked: ${it.error.message}")
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
contactsNode?.subscribe(to: .childEvent) { event in
    switch event.eventType {
    case .childAddition:
        myView.addItem(name: event.key, value: event.value.decoded())
    case .childChange:
        myView.updateItem(name: event.key, value: event.value.decoded())
    case .childRemoval:
        myView.deleteItem(name: event.key) // mark the item removed without deleting it
    case .childAcknowledgement:
        break // childAcknowledgement vents do nothing here
    default:
        break // other events are not subscribed
    }
    // darken, lighten or delete the item depending on its acknowledged and removed statuses
    myView.markItem(name: event.key, isAcknowledged: event.value.isAcknowledged) 
} onCompletion: { completion in
    if case let .revocation(reason: reason) = completion {
        print("Contact watching revoked: \(reason)")
    }
}

Common mistakes

Let's go on with our address book example, and assume it is a very simple application that allows to only add new contacts. Now we want to display a visual feedback when a newly added contact has been taken into account by the back end (ie acknowledged) and so is available for other users of the app. The first attempt often look like:

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a new contact is added
contactsNode.on(["child_added", "child_ack"],
        snapshot => {
          switch (snapshot.eventType()) {
            case "child_added":
              myView.addItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_ack":
              myView.markItem({name: snapshot.name(), acknowledged: true});
              break;
            default:
              return; // other events are not subscribed
          }
        }
);
val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
val subscription = contactsNode.subscribe(Child.Addition, includesAcknowledgement = true) {
  if (it is WebcomResult.Success) {
    when (it.result.trigger) {
      ADDED -> myView.addItem(name = it.result.node.key, value = it.result.value)
      ACKNOWLEDGED -> myView.markItem(name = it.result.node.key, acknowledged = true)
      else -> return@subscribe // other events are not subscribed
    }
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
contactsNode?.subscribe(to: .child(addition: true, acknowledgement: true)) { event in
    switch event.eventType {
    case .childAddition:
        myView.addItem(name: event.key, value: event.value.decoded())
    case .childAcknowledgement:
        myView.markItem(name: event.key, isAcknowledged: true) 
    default:
        break // other events are not subscribed
    }
}

1st problem:

Imagine another user adds a new contact, then our device receives a “Child Addition” event, which will display a new item (addItem function) in the not acknowledged state. And then... nothing more: the added item never switches to the acknowledged state!

Actually, Webcom doesn't necessarily raise a “Child Acknowledgement” event when a new child is acknowledged: when the added child is already acknowledged by the back end (it is the case when the addition results from another client), only a “Child Addition” event is raised with the acknowledged property already set to true.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a new contact is added
contactsNode.on(["child_added", "child_ack"],
        snapshot => {
          switch (snapshot.eventType()) {
            case "child_added":
              myView.addItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_ack": // "child_ack" does nothing specific
              break;
            default:
              return; // other events are not subscribed
          }
          // the ack update is common to ALL watched child events
          myView.markItem({name: snapshot.name(), acknowledged: snapshot.acknowledged()});
        }
);
val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
val subscription = contactsNode.subscribe(Child.Addition, includesAcknowledgement = true) {
  if (it is WebcomResult.Success) {
    when (it.result.trigger) {
      ADDED -> myView.addItem(name = it.result.node.key, value = it.result.value)
      ACKNOWLEDGED -> {} // "child_ack" does nothing specific
      else -> return@subscribe // other events are not subscribed
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name = it.result.node.key, acknowledged = it.result.dataAcknowledged)
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
contactsNode?.subscribe(to: .child(addition: true, acknowledgement: true)) { event in
    switch event.eventType {
    case .childAddition:
        myView.addItem(name: event.key, value: event.value.decoded())
    case .childAcknowledgement:
        break // .childAcknowledgement does nothing specific
    default:
        break // other events are not subscribed
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name: event.key, isAcknowledged: event.value.isAcknowledged) 
}

2nd problem:

Assume that we include a “date” field in each added child node (that represents a new contact in our address book) to record the date when the contact is added, and that we set this field using the “timestamp” server value (see Synchronize time).

Now I add a new contact from my local client, it appears on the GUI, but bother... again, it doesn't switch to the acknowledged state!

To understand this behavior, when the child node is added, the first raised event is “Child Addition” in the not acknowledged state, with the locally estimated timestamp in the “date” field. Then when the back end sets the actual timestamp, which is always slightly different from the estimated one, it updates this newly added child, which results in raising a “Child Change” event, this time in the acknowledged state, with the final timestamp value. Consequently, no “Child Acknowledgement” event is raised in this case. Hence, if we forget to watch the “Child Change” event, we miss the acknowledgement of this child node...

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a new contact is added
contactsNode.on(["child_added", "child_ack", "child_changed"],
        snapshot => {
          switch (snapshot.eventType()) {
            case "child_added":
              myView.addItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_changed":
              myView.updateItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_ack": // "child_ack" does nothing specific
              break;
            default:
              return; // other events are not subscribed
          }
          // the ack update is common to ALL watched child events
          myView.markItem({name: snapshot.name(), acknowledged: snapshot.acknowledged()});
        }
);
val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
val subscription = contactsNode.subscribe(Child.Addition + Child.Change, includesAcknowledgement = true) {
  if (it is WebcomResult.Success) {
    when (it.result.trigger) {
      ADDED -> myView.addItem(name = it.result.node.key, value = it.result.value)
      CHANGED -> myView.updateItem(name = it.result.node.key, value = it.result.value)
      ACKNOWLEDGED -> {} // "child_ack" does nothing specific
      else -> return@subscribe // other events are not subscribed
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name = it.result.node.key, acknowledged = it.result.dataAcknowledged)
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
contactsNode?.subscribe(to: .child(addition: true, change: true, acknowledgement: true)) { event in
    switch event.eventType {
    case .childAddition:
        myView.addItem(name: event.key, value: event.value.decoded())
    case .childChange:
        myView.updateItem(name: event.key, value: event.value.decoded())
    case .childAcknowledgement:
        break // .childAcknowledgement does nothing specific
    default:
        break // other events are not subscribed
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name: event.key, isAcknowledged: event.value.isAcknowledged) 
}

3rd problem:

Assume that we add some security rules to allow the addition of new contacts only when the user is authenticated.

Now I add a new contact without having authenticated myself. Surprise: the new contact appears on my GUI (admittedly as not acknowledged) even if the back end prevents its addition!

The explanation is similar to that of the 2nd problem above: when the child node is added it first raises a “Child Addition” event locally on the client, and when the back end forbids this addition, a “Child Removal” event is raised back to the client. So, if we don't watch this child event, we miss the fact that the back end has invalidated the new child node.

const ref = new Webcom("<your-app>");
let contactsNode = ref.child("contacts");
// Update a GUI in real-time each time a new contact is added
contactsNode.on(["child_added", "child_ack", "child_changed", "child_removed"],
        snapshot => {
          switch (snapshot.eventType()) {
            case "child_added":
              myView.addItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_changed":
              myView.updateItem({name: snapshot.name(), value: snapshot.val()});
              break;
            case "child_removed":
              myView.deleteItem({name: snapshot.name()});
              return; // no need to fallback to mark the acknowledgement status
            case "child_ack": // "child_ack" does nothing specific
              break;
            default:
              return; // other events are not subscribed
          }
          // the ack update is common to ALL watched child events
          myView.markItem({name: snapshot.name(), acknowledged: snapshot.acknowledged()});
        }
);
val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val contactsNode = manager / "contacts"
// Update a GUI in real-time each time a new contact is added
val subscription = contactsNode.subscribe(Child.Addition + Child.Change + Child.Removal, includesAcknowledgement = true) {
  if (it is WebcomResult.Success) {
    when (it.result.trigger) {
      ADDED -> myView.addItem(name = it.result.node.key, value = it.result.value)
      CHANGED -> myView.updateItem(name = it.result.node.key, value = it.result.value)
      REMOVED -> {
        myView.deleteItem(name = it.result.node.key)
        return@subscribe // no need to fallback to mark the acknowledgement status
      }
      ACKNOWLEDGED -> {} // "child_ack" does nothing specific
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name = it.result.node.key, acknowledged = it.result.dataAcknowledged)
  }
}
let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let contactsNode = manager / "contacts"
// Update a GUI in real-time each time the list of contacts is updated
contactsNode?.subscribe(to: .child(addition: true, change: true, acknowledgement: true)) { event in
    switch event.eventType {
    case .childAddition:
        myView.addItem(name: event.key, value: event.value.decoded())
    case .childChange:
        myView.updateItem(name: event.key, value: event.value.decoded())
    case .childRemoval:
        myView.deleteItem(name: event.key)
        return // no need to fallback to mark the acknowledgement status
    case .childAcknowledgement:
        break // .childAcknowledgement does nothing specific
    default:
        break // other events are not subscribed
    }
    // the ack update is common to ALL watched child events
    myView.markItem(name: event.key, isAcknowledged: event.value.isAcknowledged) 
}

As a conclusion, always keep this advice in mind:

It is very often necessary to watch ALL child events, otherwise you are likely to miss unsolicited and unexpected changes on your data.