Live Service

The Kinvey Live Service enables an event-driven way to receive data live on your device.

In the past, when an entity has been updated in a Kinvey collection, a user would have to perform a Find operation to retrieve the latest data. Using the Kinvey Live Service, updates from the Kinvey backend can instead be pushed down to the device when an entity has been updated.

In addition to the live updating of entities on your device, the Kinvey Live Service can also enable quick and secure real-time communication between users, in a publish/subscribe pattern. When a user sends a message to another user, that user will receive the message live on their device.

Kinvey Live Service is currently in Early Adopter phase! We are continuously making additions and improvements to this feature, based on early adopter feedback. Please refer to this guide for the latest updates, and check out the Current Limitations to see what will be coming soon. If you have any questions or feedback, contact us at support@kinvey.com.

Enabling Live Service

The Kinvey Live Service must be enabled on a per-app basis before it can be used. Take the following steps to enable it.

  1. Open the Kinvey Console and click your app. The app settings screen opens.
  2. In the sidebar, click the Live Service tab.

    Live Service Console Tab

  3. On the Live Service settings page, toggle the switch to On to enable the service for your app. This will allow all environments within this app to use Live Service.

    Live Service Console Enable

If at any point you want to disable Live Service for your app, navigate back to Live Service settings page and toggle the switch to Off.

Register For Live Service

After you enable Live Service, you can configure your client-side app to start using it. Before that, ensure that the app has registered an active user. After you have a user logged in to your app, use the following code to request Live Service:

guard let user = Kinvey.sharedClient.activeUser else {
    return
}
user.registerForRealtime() {
    switch $0 {
    case .success:
        print("register for realtime succeed")
    case .failure(let error):
        // handle registration errors
        print(error)
    }
}

Similarly, to stop using Live Service, unregister the user as shown below.

guard let user = Kinvey.sharedClient.activeUser else {
    return
}
user.unregisterForRealtime() {
    switch $0 {
    case .success:
        print("unregister for realtime succeed")
    case .failure(let error):
        // handle unregistration errors
        print(error)
    }
}

Collection Subscription

An app can subscribe to one or more Kinvey collections. Subscribing to a collection allows the app to receive a live feed of all entities that are created or updated in the collection.

On the DataStore corresponding to a collection, call subscribe and provide a handler. The handler exposes four actions that will be triggered when messages or errors arrive from the Live Service.

  • subscription: notifies when the subscription is ready and waiting for incoming messages
  • onNext: receives entities added or updated in this collection
  • onStatus: receives informational messages regarding Live Service
  • onError: receives any errors that occur during communication with Live Service

Status updates arrive on the onStatus method as a RealtimeStatus enum. This enum indicates informational events such as connecting or disconnecting to Live Service.

Whereas the onStatus callback gets called with informational updates, the onError method gets invoked when an exception has occurred with the Kinvey Live Service. These error include events such as not being able to subscribe to Live Service, or permission errors when attempting to use Live Service.

let dataStore = DataStore<Book>.collection()

dataStore.subscribe(subscription: {
    // handle subscription, waiting for incoming messages
}, onNext: { result in
    // handle new real-time messages
}, onStatus: { status in
    // handle subscription status changes
}, onError: { error in
    // handle errors
})

To stop receiving live updates from a collection, simply unsubscribe.

dataStore.unsubscribe() {
    switch $0 {
    case .success:
        print("unsubscribe succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Besides unsubscribe(), calling logout() also terminates the user's subscription to all collections and streams. The difference is that, in addition, logging out disables Live Service for the user. Don't forget to reenable it and resubscribe the user after you log them back in if you want to keep the subscriptions.

User-to-user communication

Certain app use cases require that users or devices communicate directly with one another, allowing for faster communication than is possible using collection subscriptions. The user-to-user communication feature of the Kinvey Live Service offers one way of approaching these types of use cases. Continue reading to understand this feature, and to see if it can help you when building your solution.

User-to-user communication is accomplished using a LiveStream object. You can think of a LiveStream as an object that controls the communication of a specific type of message using the Kinvey Live Service.

A LiveStream object is mapped to a stream that you create on the backend (using the Console) and has a number of responsibilities:

  • Encapsulate a specific type of message to be passed among users.
  • Grant access rights to specific users of the stream, controlling who can send and receive messages.
  • Provide APIs for communicating messages. The API you use will depend on how you model your user-to-user communication.

Each of these responsibilities, along with creating a stream in the Console, is discussed in greater detail below.

Stream Message Type and Creation

When you create a LiveStream, you associate a user-defined class with it. This class defines the schema of the messages that are sent and received using this LiveStream, representing a specific type of message. For example, say that we are modeling song recommendation messages, represented as a SongRecommendation class:

// Live Service Stream Type
import ObjectMapper

struct SongRecommendation: StaticMappable {

    var name: String?
    var artist: String?
    var rating: Int?

    static func objectForMapping(map: Map) -> BaseMappable? {
        return SongRecommendation()
    }

    mutating func mapping(map: Map) {
        name <- map["song_name"]
        artist <- map["song_artist"]
        rating <- map["rating"]
    }

}

To create a stream that will be used to pass messages of the SongRecommendation type, we begin by creating a stream on the backend, through the Console. Navigate to the environment dashboard for an app that has Live Service enabled, and click the Live Service menu item.

Live Service Console Menu Item

Selecting this will display the Live Service environment settings page, which has sections for both Collection Subscription and User-to-User Communication.

Live Service Console View

Click on the Add a Stream button to create a stream, and then follow the instructions and fill in the necessary information.

Live Service Create Stream

Click Save to finish creating the SongRecommendation stream.

Live Service Create Stream

Now that we created a stream on the backend, we can create a matching LiveStream object in our client app, and connect the two.

// Create stream object corresponding to "SongRecommendation" stream via the backend
let stream = LiveStream<SongRecommendation>(name: "SongRecommendation")

After this step, we have a stream object for sending and receiving SongRecommendation messages. The next step would be to decide how you want to model your user-to-user communication.

Communication Models

There are two primary ways to model user-to-user communication: Directed Communication or Feed Communication. You will typically choose one of these, depending on the use case you are trying to solve. We detail these communication models below. For each model, we discuss the pattern represented by it, explain how to set appropriate access rights on a LiveStream to support that model, and highlight the API methods you will use.

Directed Communication

The first, and perhaps most straightforward approach to modeling user-to-user communication, is Directed Communication. In this approach, each user can create a message and send it to another user, who is listening for incoming messages. The diagram below illustrates an example of this involving three users, and the communication relationships among them.

User-to-user Directed Communication

In this example, we want John to be able to send directed messages to Paul, and we also want George to send directed messages to Paul. Likewise, we want Paul to listen to any messages sent to him. We also want Paul to send messages to George. Lastly, we want George to listen to messages sent to him. The key in implementing this behavior is to grant the correct access on the LiveStream to the appropriate users.

Granting Access to Streams

By default, users do not have access to send or receive messages over a stream. Regardless of whether the communication model is Directed or Feed, users must be granted the appropriate access rights to the stream before they can communicate.

Granting access on a stream is done on a per-user, directional basis. This means that you specify who can send messages to a user, as well as specify who can receive messages that are sent to a user. To illustrate this, let us first grant the correct access rights for Paul on this stream. According to the diagram, we want both John and George to be able to send messages to Paul, and we want Paul to be able to receive messages sent to him.

To do this, we create and update a LiveStreamAcl object, which allows us to specify publishers (users who can send messages to Paul) and subscribers (users who can receive messages sent to Paul) by user ID. In the case of the example above, only Paul will be able to subscribe to messages sent to him. After creating the LiveStreamAcl, we use the grantStreamAccess() method on our stream object to apply these access privileges to Paul.

// Grant stream access for Paul:
//   - John and George can send messages to Paul
//   - Paul can listen to messages sent to himself

var acl = LiveStreamAcl()

acl.publishers.append(john.userId)
acl.publishers.append(george.userId)

acl.subscribers.append(paul.userId)

stream.grantStreamAccess(userId: paul.userId, acl: acl) {
    switch $0 {
    case .success:
        print("grant stream access succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Similarly, we can set up the correct access for George. Paul can send messages to George, and George can listen to messages sent to himself. As emphasized by this last part, remember that since no users can send or receive messages until they are granted the appropriate access, we must grant George access to subscribe to himself.

// Grant stream access for George:
//   - Paul can send messages to George
//   - George can listen to messages sent to himself

var aclGeorge = LiveStreamAcl()

aclGeorge.publishers.append(paul.userId)

aclGeorge.subscribers.append(george.userId)

stream.grantStreamAccess(userId: george.userId, acl: aclGeorge) {
    switch $0 {
    case .success:
        print("grant stream access succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Sending Messages and Listening

At this point, the LiveStream has been properly configured for access. Now the LiveStream can be used to communicate messages. In the Directed Communication model, the API model used is send/listen. The user who is sending messages specifies the message receiver and the receiver listens for any messages sent to them.

Referencing the diagram and the SongRecommendation type above, John can send Paul a SongRecommendation message using the send() method:

let songRecommendation = SongRecommendation(name: "Imagine", artist: "John Lennon", rating: 100)

stream.send(userId: receiverId!, message: songRecommendation) { (result: Result<Void, Swift.Error>) in
    switch result {
    case .success:
        print("song recommendation sent!")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Likewise, Paul can listen to any stream messages that are sent to him using the listen() method:

stream.listen(listening: {
    // handle listening, waiting for incoming messages 
}, onNext: { (songRecommendation) in
    // handle receiving new song recommendation
}, onStatus: { (realtimeStatus) in
    // handle status changes
}, onError: { (error) in
    // handle errors
})

Here you will notice that a handler object is passed into the listen method. The onNext block will receive the messages that are sent to this user. The handler exposes four actions that will be triggered when messages or errors arrive from the Live Service.

  • listening: notifies when the listening is ready and waiting for incoming messages
  • onNext: receives real-time messages of the specified type, and their corresponding sender user ID
  • onError: receives any errors that occur during communication with the Live Service
  • onStatus: receives informational messages regarding the Live Service

The onStatus and onError blocks are similar to those for the DataStore<Type>.subscribe() method. These blocks are described in the Collection Subscription section.

If, at some point in time, Paul wants to stop receiving messages sent to him, he can stop listening to the stream:

stream.stopListening {
    switch $0 {
    case .success:
        print("stop listening succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Feed Communication

While Directed Communication allows for communication by explicitly deciding who receives a message, there may be cases where you want a user to send a single message to be read by a number of other users, without explicitly sending the message to each user. This behavior is more intuitively modeled by Feed Communication.

In the Feed Communication model, instead of sending a message to someone specific, the user sends the message to anyone who is listening to them. Other users interested in those messages would then watch, or follow, that user to see those messages.

This is accomplished using a post/follow API model, where the user who is sending the message would post and other users who want to see the message would follow the posting user.

User-to-user Feed Communication

In the diagram above, both John and Paul post messages to anyone who listen. George wants to see messages from both John and Paul, so he follows them both. Paul would only like to see messages from John, so he follows only him.

Granting Access to Streams

To allow for Feed Communication, we need to add the correct access rights to the stream in question. In Feed Communication, the only one who is allowed to publish to a user is that user himself, because he is "posting" the message to anyone following them. The subscribers are other users who want to follow the posting user. In the case of John, he will be allowed to publish to himself (post messages), while both Paul and George will be allowed to subscribe to John (follow him).

// Grant stream access for John:
//   - John can send messages to himself (post)
//   - Paul and George can listen to messages sent to John (follow)

var aclForJohn = LiveStreamAcl()

aclForJohn.publishers.append(john.userId)

aclForJohn.subscribers.append(paul.userId)
aclForJohn.subscribers.append(george.userId)

stream.grantStreamAccess(userId: john.userId, acl: aclForJohn) {
    switch $0 {
    case .success:
        print("grant stream access succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

For Paul, he will similarly be able to post to himself, and George can follow him.

// Grant stream access for Paul:
//   - Paul can send messages to himself (post)
//   - George can listen to messages sent to Paul (follow)

var aclForPaul = LiveStreamAcl()

aclForPaul.publishers.append(paul.userId)

aclForPaul.subscribers.append(george.userId)

stream.grantStreamAccess(userId: paul.userId, acl: aclForPaul) {
    switch $0 {
    case .success:
        print("grant stream access succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

The example shows well where the advantage of Feed Communication stands for this use case. As more users want to follow the posts of a specific user, all you have to do is change permissions for the posting user to include these new followers. After that, when the new users follow the posting user, they will automatically see new posts.

Posting Messages and Following Users

After we grant the appropriate Feed Communication access, John can post() new messages to all users who follow him. In the example below, he posts a new song recommendation.

let songRecommendation = SongRecommendation(name: "Strawberry Fields Forever", artist: "The Beatles", rating: 95)

stream.post(message: songRecommendation) { result in
    switch result {
    case .success:
        print("song recommendation sent!")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Paul would have to follow John to see his song recommendations, which will take the following code:

stream.follow(userId: john.userId, following: {
    // handle following, waiting for incoming messages 
}, onNext: { (songRecommendation) in
    // handle receiving new song recommendation
}, onStatus: { (realtimeStatus) in
    // handle status changes
}, onError: { (error) in
    // handle errors
})

If Paul no longer wants to see John's song recommendations, he can unfollow() John.

stream.unfollow(userId: john.userId) { result in
    switch result {
    case .success:
        print("unfollow succeed")
    case .failure(let error):
        // handle errors
        print(error)
    }
}

Getting and Updating Stream ACLs

In both Directed and Feed communication, sometimes you may want to update a user's ACL for a stream, to give the user more or less access rights. In this case it makes sense to first read the user's current ACL and use is as a basis instead of building it up from zero.

To read a user's ACL for a stream, call the streamAccess() method. It returns the same ACL object that you receive as response when you use grantStreamAccess().

The next code example adds a subscriber to Paul's ACL without affecting access rights that are already in place. Note that you can use the get method's return value and pass it as an argument when initializing a new stream ACL object. This ensures that all the user's current access rights are passed on and allows you to easily add to them.

let stream = LiveStream<SongRecommendation>(name: "SongRecommendation")
let user: User = <#...#>

stream.streamAccess(userId: user.userId) {
    switch $0 {
    case .success(let acl):
        print("ACL: \(acl)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Current Limitations

The Kinvey Live Service feature is currently in its Early Adopter phase, and while we are continuously adding features and fixing issues as we move towards general availability, there are still limitations and features not yet implemented.

  • Limitations:
    • Message size: Entities that are larger than 32 KB are not sent through the Kinvey Live Service.
    • Business Logic: Data changes originating from Business Logic do not trigger any of the collection subscription events.
  • Features not yet implemented:
    • Collection Subscription: Notifications about deleted entities.
    • Offline and Caching: Data received from Live Service is not cached neither persisted for offline use in Local Storage.
Got a question?