Tutorial: Subscribable events in depth

Serverless DatabaseData StructureSubscribe to changes Subscribable events in depth

Current limitations

When subscribing to some events on a given data node, the main limitation affects the Child Acknowledgement event, which currently cannot be raised to acknowledge the removal of a child node when this removal results from setting the parent node with a value that involves no child.

Assume we have the {a:"foo"} value on the watched node. If we set this node with "foo", {} or null (these values are “leaf” nodes with no child), the watched node will receive a Child Removal event for child a in the not (yet) acknowledged state, but no subsequent Child Acknowledgement event once acknowledged by the back end.

However, if we had set {b:"bar"} instead (this value is a “tree” node with one child), then the watched node would have received in addition a Child Acknowledgement event for child a once acknowledged by the back end.

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.