Tutorial: Subscribing to changes

Serverless DatabaseData Structure Subscribing 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 property includesAcknowledgements of the options parameter. The subscribed types of event are given as their common Kotlin type inheriting the SubscribableEvent interface: Value.Changed::class for the “Value Change” event type, Child.Added::class for the “Child Addition” event type, Child.Changed::class for the “Child Change” event type, etc...
  • The last argument of the subscribe() method is a callback function, which is called for each raised event, including cancelation and revocation events.

The notifications received by the callback may be either:

  • A cancelation or revocation notification (passed as instances of Notification.ControlNotification subclasses). In this case, it is the last notification to be received by the callback.
  • A notification of one of the subscribed events (which are all subclasses of both Notification.DataNotification and SubscribableEvent). Among the properties common to all of these subclasses, isAcknowledgement indicates whether the event is one of the “Value Acknowledgement” or “Child Acknowledgement” events, and value gives the DatasyncValue associated to the event (including its acknowledgement status).
import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

val app = WebcomApplication.default
val manager = app.datasyncService.createManager()
val node = manager / "contacts"
val options = SubscriptionOptions(includesAcknowledgements = true)
val subscription = node.subscribe(Value.Changed::class, options = options) { // it: Notification<Value.Changed>
  when (it) {
    is Notification.DataNotification -> { // it.data: Value.Changed
      if (!it.data.isAcknowledgement) println("The data at CONTACTS has changed: ${it.data.value}")
      println("The data at CONTACTS ${if (it.data.value.acknowledged) "is" else "is not"} acknowledged")
    }
    is Notification.ControlNotification ->
      println("The subscription on CONTACTS has been canceled or revoked! ${it.error?.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.

Not available in 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:

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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(Value.Changed::class) { // it: Notification<Value.Changed>
  when (it) {
    is Notification.DataNotification -> // it.data: Value.Changed
      println("There are now ${it.data.value.asMap.size} contacts")
    is Notification.ControlNotification -> 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, you don't subscribe explicitly to the “Value Acknowledgement” event type. Instead, you must subscribe to the Value.Changed (or Value.ChangedHidingData) event and pass SubscriptionOptions with the includesAcknowledgement property set to true to the subscribe() method.

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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
val options = SubscriptionOptions(includesAcknowledgements = true)
contactsNode.subscribe(Value.Changed::class, options = options) { // it: Notification<Value.Changed>
  if (it is Notification.DataNotification && it.data.value.acknowledged) 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.Added interface:

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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.Added::class) { // it: Notification<Child.Added>
  when (it) {
    is Notification.DataNotification -> // it.data: Child.Added
      myView.addItem(name = it.data.node.key, value = it.data.value.asA(Contact::class))
    is Notification.ControlNotification -> println("Contact watching revoked: ${it.error?.message}")
  }
}

If needed, the previousKey property gives the key of the preceding child node or null if the subscriber node is in first position.

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.Changed interface:

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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.Changed::class) { // it: Notification<Child.Changed>
  when (it) {
    is Notification.DataNotification -> // it.data: Child.Changed
      myView.updateItem(name = it.data.node.key, value = it.data.value.asA(Contact::class), after = it.data.previousKey)
    is Notification.ControlNotification -> println("Contact watching revoked: ${it.error?.message}")
  }
}

As with Child.Added, the previousKey property gives the key of the preceding child node or null if the subscriber node is in first position.

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.Removed interface:

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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.Removed::class) { // it: Notification<Child.Removed>
  when (it) {
    is Notification.DataNotification -> // it.data: Child.Removed
      myView.deleteItem(name = it.data.node.key)
    is Notification.ControlNotification -> 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 the “Value Acknowledgement” event type, in Kotlin, you don't subscribe explicitly to the “Child Acknowledgement” event type. Instead, you must subscribe to one or more child events and pass SubscriptionOptions with the includesAcknowledgement property set to true to the subscribe() method.

import com.orange.webcom.sdkv2.datasync.subscription.SubscribableEvent.*

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 options = SubscriptionOptions(includesAcknowledgements = true)
val subscription = contactsNode.subscribe(Child.AddedChangedRemoved::class, options = options) { // it: Notification<Child.AddedChangedRemoved>
  when (it) {
    is Notification.DataNotification -> with(it.data) { // this: Child.AddedChangedRemoved
      // this is an instance of either Child.Added, Child.Changed, Child.Removed or Child.Acknowledged
      if (!isAcknowledgement) when (this) { // Child.Acknowledged inherits all Child.* interfaces, so it must be checked first
        is Child.Added -> myView.addItem(name = node.key, value = value.asA(Contact::class))
        is Child.Changed -> myView.updateItem(name = node.key, value = value.asA(Contact::class), after = previousKey)
        is Child.Removed -> myView.deleteItem(name = node.key) // mark the item removed without deleting it
      }
      // darken, lighten or delete the item depending on its acknowledged and removed statuses
      myView.markItem(name = node.key, acknowledged = value.acknowledged)
    }
    is Notification.ControlNotification -> println("Contact watching revoked: ${it.error?.message}")
  }
}

As the Child.Acknowledged interface inherits all Child.Added, Child.Changed and Child.Removed ones. Consequently, you have to check whether an event is Child.Acknowledged BEFORE performing any pattern matching against one of these interfaces.
To do so, simply test the isAcknowledgement property.

Note also how you can subscribe to several child events: there is one interface available for each possible combination: Child.AddedChanged for subscribing to both “Child Addition” and “Child Change” events, Child.AddedRemoved for both “Child Addition” and “Child Removal”, etc...

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