Presence
Presence enables clients to be aware of other clients that are currently “present” on a channel. Each member present on a channel has a unique self-assigned client identifier and system-assigned connection identifier, along with an optional payload that can be used to describe the member’s status or attributes. Presence enables you to quickly build apps such as chat rooms and multiplayer games by automatically keeping track of who is present in real time across any device.
Other devices and services can subscribe to presence events in real time using the realtime interface or with integrations. You can also request a list of clients or devices on a channel at a particular point in time with the REST interface.
Trigger presence events
Whenever a member enters or leaves a channel, or updates their member data, a presence event is emitted to all presence subscribers on that channel.
The following presence events are emitted:
- Enter
- A new member has entered the channel
- Leave
- A member who was present has now left the channel. This may be a result of an explicit request to leave or implicitly when detaching from the channel. Alternatively, if a member’s connection is abruptly disconnected and they do not resume their connection within a minute, Ably treats this as a leave event as the client is no longer present
- Update
- An already present member has updated their member data. Being notified of member data updates can be very useful, for example, it can be used to update the status of a user when they are typing a message
- Present
- When subscribing to presence events on a channel that already has members present, this event is emitted for every member already present on the channel before the subscribe listener was registered
Member data
In addition to the clientId
for members on a channel, it is also possible to include data when entering a channel. Clients can update their data at any point which will be broadcasted to all presence subscribers as an update
event.
The following is an example of subscribing to enter and leave events, then entering the presence set and updating a client’s member data:
/* Subscribe to presence enter events */
await channel.presence.subscribe('enter', (member) => {
console.log(member.data); // => not moving
});
/* Subscribe to presence update events */
await channel.presence.subscribe('update', (member) => {
console.log(member.data); // => travelling North
});
/* Enter this client with data and update once entered */
await channel.presence.enter('not moving');
await channel.presence.update('travelling North');
CopyCopied!
Managing multiple client IDs
An Ably client instance might, if on an application server for example, publish messages and be present on channels on behalf of multiple distinct client IDs. The channel’s Presence
object therefore also supports methods that enable presence messages to be emitted for a clientId
specified at the time of the call, rather than implicitly based on the clientId
specified when the SDK is instantiated or authenticated.
Each unique clientId
may only be present once when entering on behalf of another client as the unique identifier for each member in a presence set is the combined clientId
and shared connectionId
>.
A single clientId
may be present multiple times on the same channel via different client connections. Ably considers these as different members of the presence set for the channel, however they will be differentiated by their unique connectionId. For example, if a client with ID “Sarah” is connected to a chat channel on both a desktop and a mobile device simultaneously, “Sarah” will be present twice in the presence member set with the same client ID, yet will have two unique connection IDs. A member of the presence set is therefore unique by the combination of the clientId and connectionId strings.
In order to be able to publish presence changes for arbitrary client IDs, the SDK must have been instantiated either with an API key, or with a token bound to a wildcard client ID.
const rest = new Ably.Rest({ key: '<loading API key, please wait>' });
/* request a wildcard token */
const token = await rest.auth.requestToken({ clientId: '*' });
const realtime = new Ably.Realtime({ token: token });
const channel = realtime.channels.get('realtime-chat');
await channel.presence.subscribe('enter', (member) => {
console.log(member.clientId + ' entered realtime-chat');
});
await channel.presence.enterClient('Bob'); // => Bob entered realtime-chat
await channel.presence.enterClient('Mary'); // => Mary entered realtime-chat
Demo OnlyCopyCopied!
Subscribe to presence events
Subscribe to a channel’s presence set in order to receive presence events, by registering a listener. Presence events are emitted whenever a member enters or leaves the presence set, or updates their member data.
Subscribing is an operation available to the realtime interface of an Ably SDK and uses the subscribe() method on the Presence
object of a channel.
The following is an example of subscribing to presence events and entering the presence set:
const realtime = new Ably.Realtime({
key: '<loading API key, please wait>',
clientId: 'bob'
});
const channel = realtime.channels.get('nil-box-fog');
await channel.presence.subscribe('enter', (member) => {
alert('Member ' + member.clientId + ' entered');
});
await channel.presence.enter();
Demo OnlyCopyCopied!
Messages are streamed to clients as soon as they attach to a channel, as long as they have the subscribe capability.
Efficient use of capabilities can prevent clients from receiving presence events while still allowing them to enter the presence set. This is a common scenario where only a server is required to monitor the presence set. This saves clients from subscribing to a potentially high volume of unnecessary messages.
One example of achieving this would be to use one channel for generic communications and another for the presence set. The following capabilities demonstrate this for clients and for servers:
For clients:
{
"chatPresence": ["presence"],
"chat": ["publish", "history", "subscribe"],
}
CopyCopied!
For servers:
{
"chatPresence": ["presence", "subscribe"],
"chat": ["publish", "history", "subscribe"],
}
CopyCopied!
Alternatively, channel mode flags can be used to enable clients to be present on a channel without subscribing to presence events. This also enables clients to still subscribe to regular messages on the channel.
Retrieve presence members
The membership of the presence set can be retrieved by calling the get()
method on the Presence
object of a channel. This returns an array of all members currently present on the channel and is available using the REST and realtime interfaces of an Ably SDK.
An Ably client connected using the realtime interface of an SDK is responsible for keeping track of the presence set from the time that the channel is attached. An up to date presence set is pushed to the client following a channel attachment, and the presence set is updated on each subsequent presence event. This means that get()
returns the already known presence set retained in memory and does not trigger a new request to the Ably service.
The REST interface of an Ably SDK queries the REST API directly. No presence state is cached in the SDK itself.
The following is an example of retrieving the presence set for a channel:
const presenceSet = await channel.presence.get();
if (presenceSet.length > 0) {
console.log('There are ' + presenceSet.length + ' members on this channel');
console.log('The first member has client ID: ' + presenceSet[0].clientId);
} else {
console.log('There are no members in this channel');
}
CopyCopied!
Synced presence set
A common requirement of the presence set is to keep an updated list of members that are currently present on a channel in your user interface.
Many developers try to build the initial list using the get()
method and then mutate that list whenever a new presence event arrives. Ably advises against this approach, because it’s easy to quickly go wrong and end up with a list that’s out of sync with the real presence set.
One common error is to fail to take into account that a single clientId
may be present multiple times on the same channel via different connections. As far as Ably is concerned, these are different members of the presence set as they have different connectionId
s. For example, if a client with the ID “Sarah” is connected to a channel on both a desktop and a mobile device simultaneously, or via multiple tabs in a browser, “Sarah” will be present twice in the presence set with the same clientID
. If “Sarah” leaves the channel on her mobile, your app sees the leave
event and incorrectly removes her entry from the list.
Ably recommends that you instead just get()
the presence set afresh from the Ably SDK whenever you see a presence event and use it to rebuild the list of members in your user interface. The get()
operation is free and a local call to the SDK and provides you the presence set that the client has already synced with the server. The server keeps the presence set up to date and there is not cost to using this approach.
The following is an example of calling the get()
method on presence messages:
await channel.presence.subscribe(async (presenceMessage) => {
// Ignore the presence message, just rebuild your state from the get() method
// (including uniquifying by clientId if you only want one entry per clientId)
const presenceSet = await channel.presence.get();
if (presenceSet.length > 0) {
console.log('There are ' + presenceSet.length + ' members on this channel');
} else {
console.log('There are no members on this channel');
}
});
CopyCopied!
Batch presence
It is possible to retrieve the presence state of multiple channels in a single request. The presence state contains details about the clients in the presence set, such as their clientId
, member data and presence action.
A batch request has a single set of request details containing the request body, parameters and headers. These are converted into an array of requests to the underlying API. Each individual request to the underlying API is performed in parallel and may succeed or fail independently.
The following is an example of a batch presence request using the request() method to query the batch REST API
const ablyRest = new Ably.Rest({ key: '<loading API key, please wait>' })
const content = { 'channel': 'channel1,channel2' }
const presenceSets = await ablyRest.request('GET', '/presence', null, content, null);
console.log('Success! status code was ' + presenceSets.statusCode);
Demo OnlyCopyCopied!
Batch presence requests
A batch presence request contains a comma separated list of channel names with no spaces.
The following is an example of a batch presence request using the request() method to query the batch REST API
var ablyRest = new Ably.Rest({ key: '<loading API key, please wait>' })
const content = { 'channel': 'quickstart,channel2' }
const presenceSets = await rest.request('GET', '/presence', content, null, null);
console.log('Success! status code was ' + presenceSets.statusCode);
Demo OnlyCopyCopied!
The following is an example curl request, querying the REST API directly:
curl -X GET https://rest.ably.io/presence?channel=quickstart,channel2 \
-u "<loading API key, please wait>"
Demo OnlyCopyCopied!
Batch presence responses
Once all requests have been completed in a batch request, a batch response is returned with three possible outcomes:
- Success
- If all of the individual requests were successful then an array containing the response of each query is returned in request order.
- Failure
- If the batch request itself failed before the individual requests were made, then an error response is returned with a status code and error response body. Examples of why the batch request can fail include an authorization failure or an invalid request.
- Partial success
- If one or more of the individual requests failed the response body contains an error object with the error code
40020
and a status code of400
. The error body contains abatchResponse
array of each individual response in request order. ThebatchResponse
can be inspected if there is a need to know the details of each outcome. If you only need to know whether or not the batch request was completely successful then the status code is sufficient.
The examples for each possible outcome will use the following request data:
{
channel: 'channel0,channel1,channel2'
}
CopyCopied!
The following is an example of a successful batch presence response. The response body contains details of each client present on the channel and the channel they are present on. The status code for a successful response is always 200
:
[
{
"channel":"channel0",
<a href="[">presence</a>
{"clientId": "user1", "action": "1"},
{"clientId": "user2", "action": "1"}
]
},
{
"channel":"channel1",
<a href="[">presence</a>]
},
{
"channel":"channel2",
<a href="[">presence</a>
{"clientId": "user2", "action": "1"},
{"clientId": "user3", "action": "1"}
]
}
]
CopyCopied!
The following is an example of a batch presence failure response. The response body contains the details of the error
, in this example that the token used for the request has expired. The status code is 401
:
{
"error": {
"message":"Token expired",
"statusCode":401,
"code":40140
}
}
CopyCopied!
The following is an example of a batch presence partial success response. The successful requests contain the details of each client present on the channel, and the channel they are present on. The failed request contains the channel the request failed for and the details of the error
, in this example that the credentials used didn’t have the capability to query that channel. The status code for a partial success is always 400
:
{
"error": {
"message": "Batched response includes errors",
"statusCode":400,
"code":40020
}
"batchResponse": [
{
"channel":"channel0",
<a href="[">presence</a>
{"clientId": "user1", "action": "1"},
{"clientId": "user2", "action": "1"}
]
},
{
"channel":"channel1",
<a href="[">presence</a>]
},
{
"channel":"channel2",
"error": {
"message": "Given credentials do not have the required capability",
"statusCode": 401,
"code": 40160
}
}
]
}
CopyCopied!
Using the request() method with the REST interface of an Ably SDK requires handling responses for success, failure and partial success.
A response
object will have response.success
set to true if the batch presence request was successful, and false if at least one individual presence request failed. The response.statusCode
will be set to 200 for success, 400 for partial success and 401 for an expected failure. response.errorCode
will then contain the Ably error code and response.errorMessage
will contain the details of the error.
response.items
will contain a list of responses for each channel for which presence has been requested in a successful response. response.items.batchResponse
will contain a list of each channel’s results, be it an error or a success for a partially successful response. The response.errorCode
will always be 40020
for a partial success.
The following is an example of handling the various responses:
var ablyRest = new Ably.Rest({ key: '<loading API key, please wait>' })
const content = { 'channel': 'channel1,channel2' }
const presenceSets = await ablyRest.request('GET', '/presence', null, content, null);
console.log(presenceSets.success);
console.log(presenceSets.errorCode);
if (presenceSets.success) {
// If complete success
for (i = 0; i < presenceSets.items.length; i++) {
// Each presenceSets item will be roughly of the style:
/*
{
'channel': 'channel1',
'presence': [
{ 'action': 1, 'clientId': 'CLIENT1' },
{ 'action': 1, 'clientId': 'CLIENT2' }
]
}
*/
}
} else if (presenceSets.errorCode === 40020) {
// If partial success
for (i = 0; i < presenceSets.items[0].batchResponse.length; i++) {
// Each batchResponse item will either be the same as success if it succeeded, or:
/*
{
'channel': 'channel1',
'error': {
'code': 40160,
'message': 'ERROR_MESSAGE',
'statusCode': 401
}
}
*/
}
} else {
// If failed, check why
console.log(presenceSets.errorCode + ', ' + presenceSets.errorMessage);
}
Demo OnlyCopyCopied!
Presence history
History provides access to instantaneous “live” history as well as the longer term persisted history for presence channels. If persisted history is enabled for the channel, then presence events will be stored for 24 or 72 hours, depending on your account package. If persisted history is not enabled, Ably retains the last two minutes of presence event history in memory.
Any time a channel is re-attached and the presence set is re-synced, e.g. after a short disconnection, the client will check whether any members it has entered into the presence set are there. If not, it will automatically re-enter them. This means that if a channel loses continuity (for example, because a client was disconnected from Ably for more than two minutes before reconnecting), then after the channel automatically re-attaches, any presence members it had previously entered will be restored.
The exception is if you use the recover feature to resume a previous connection with a fresh SDK instance (for example, to have continuity over a page refresh). In that case you will need to explicitly re-enter presence after you re-attach to the channel, due to the loss of SDK internal state.
Clients that are part of a presence set remain present for 15 seconds after they are abruptly disconnected, for example where the internet connection suddenly drops or the client is changing networks. This delay is to avoid repeated leave
and enter
events being sent when a client is experiencing an unstable connection.
The Ably SDK will attempt to reconnect after a disconnect. If the connection is reestablished before 15 seconds have passed, a leave
event will not be sent. If the connection is reestablished after 15 seconds, a leave
event will be sent and the presence set will need to be rejoined.
Note that the 15 second delay from being removed from the presence set is only for abrupt or unplanned disconnects. If a client calls leave()
or close()
they immediately send a leave
event.
The time taken before a leave
event is sent in the case of an abrupt disconnect can be reduced to a minimum of 1 second by setting a value for remainPresentFor
, in milliseconds. This property is set within the transportParams
property of the clientOptions
object.
It is important to note that it can initially take up to 30 seconds to identify that a client has been abruptly disconnected. Shorten the amount of time taken to identify abrupt disconnects using the heartbeatInterval
property if your app needs to quickly identify presence set members being abruptly disconnected.
The following example code demonstrates establishing a connection to Ably with remainPresentFor
set to 1 second:
const ably = new Ably.Realtime(
{
key: '<loading API key, please wait>',
transportParams: { remainPresentFor: 1000 }
}
);
Demo OnlyCopyCopied!