User presence and audience
The audience is the collection of users connected to a container. When your app creates a container using a service-specific client library, the app is provided with a service-specific audience object for that container as well. Your code can query the audience object for connected users and use that information to build rich and collaborative user presence features.
This document will explain how to use the audience APIs and then provide examples on how to use the audience to show user presence. For anything service-specific, the Tinylicious Fluid service is used.
Working with the audience
When creating a container, your app is also provided a container services object which holds the audience. This audience is backed by that same container. The following is an example. Note that client
is an object of a type that is provided by a service-specific client library.
const { container, services } = await client.createContainer(containerSchema);
const audience = services.audience;
The IMember
Audience members exist as IMember
objects:
export interface IMember {
id: string;
connections: IConnection[];
}
An IMember
represents a single user identity. IMember
holds a list of IConnection
objects, which represent that audience member's active connections to the container. Typically a user will only have one connection, but scenarios such as loading the container in multiple web contexts or on multiple computers will also result in as many connections. An audience member will always have at least one connection. Each user and each connection will both have a unique identifier.
Connections can be short-lived and are not reused. A client that disconnects from the container and immediately reconnects will receive an entirely new connection. The audience will reflect through its member leaving and member joining events.
Service-specific audience data
The ServiceAudience
class represents the base audience implementation, and individual Fluid services are expected to extend this class for their needs. Typically this is through extending IMember
to provide richer user information and then extending ServiceAudience
to use the IMember
extension. For TinyliciousAudience
, this is the only change, and it defines a TinyliciousMember
to add a user name.
export interface TinyliciousMember extends IMember {
userName: string;
}
Because audience data is service-specific, code that interacts with audience may be less portable to other services.
APIs
getMembers
The getMembers
method returns a map of the audience's current members. The map keys are user IDs (i.e. the IMember.userId
property), and values are the IMember
objects for the corresponding user IDs. Your code can further query the individual IMember
objects for their client connections.
The map returned by getMembers
represents a snapshot in time and will not update internally as members enter and leave the audience. Instead of holding onto the return value, your code should subscribe to ServiceAudience
's events for member changes.
getMyself
The getMyself
method returns the IMember
object from the audience corresponding to the current user calling the method. It does so by matching the container's current client connection ID with one from the audience.
Connection transitions can result in short timing windows where getMyself
returns undefined
. This is because the current client connection will not have been added to the audience yet, so a matching connection ID cannot be found. Similarly, offline scenarios may produce the same behavior.
Events
membersChanged
The membersChanged
event is emitted whenever a change to the audience members' client connections is made and will always be paired with a memberAdded
or memberRemoved
event. Listeners may call the getMembers
method to get the new list of members and their connections. Listeners that need the specific changed member or connection should use the memberAdded
and memberRemoved
events instead.
memberAdded
The memberAdded
event is emitted whenever a client connection is added to the audience. The event also provides the connection client ID and the IMember
object for this change. The IMember
object may be queried for more information on the new connection using the provided connection client ID. Depending on if it already had previous connections, the IMember
object may be either new or existing.
memberRemoved
The memberRemoved
event is emitted whenever a client connection leaves the audience. The event also provides the connection client ID and the IMember
object for this change. The IMember
object reflects its state in the audience before the connection's removal, and may be queried for more information on the removed connection using the provided connection client ID.
Using audience to build presence features
Data management and inter-user communication
While the audience is the foundation for user presence features, the list of connected users does not provide a compelling experience on its own. Building compelling presence features will involve working with additional user data. These data typically fit into one or more of the categories below.
Shared persisted data
Most presence scenarios will involve data that only a single user or client knows and needs to communicate to other audience members. Some of those scenarios will require the app to save data for each user for future sessions. For example, consider a scenario where you want to display how long each user has spent in your application. An active user's time should increment while connected, pause when they disconnect, and resume once they reconnect. This means that the time each user has spent must be persisted so it can survive disconnections.
One option is to use a SharedMap
object with a SharedCounter
object as the value onto which each user will increment their time spent every minute (also see Introducing distributed data structures). All other connected users will then receive changes to that SharedMap automatically. Your app's UI can display data from the map for only users present in the audience. A returning user can find themselves in the map and resume from the latest state.
Shared transient data
Many presence scenarios involve data that are short-lived and do not need to be persisted. For example, consider a scenario where you want to display what each user has selected in the UI. Each user will need to tell other users their own information -- where they clicked -- but the past data are irrelevant.
You can address this scenario using DDSes in the same way as with the persisted data scenario. However, using DDSes results in storage of data that are neither useful long term nor in contention among multiple users or clients. Signals are designed for sending transient data and would be more appropriate in this situation. Each user can broadcast a signal containing their selection data to all connected users, and those users can store the data locally. Newly connected users can request other connected users to send their selection data using another signal. When a user disconnects, the local data are discarded.
Unshared data
In some cases, the user data could be generated locally or fetched from an external service. For example, consider a scenario where you want to display the connected users with a profile picture and a color border. If your app retrieves a user's profile picture from a user metadata service and assigns each user a color based on a hash of their user ID, then the app will have the desired data on other users without needing to communicate with them.