Warning: You are viewing an old version (0.8) of this documentation. We recommend you view the latest version 1.2.
Realtime Client Library API

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 allows 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.

Getting started

The Presence object provides a straightforward API to subscribe to presence events such as members entering or leaving, retrieve a list of members present, or register the connected client as “present” on a channel. Here is a simple presence example using the presencePresence propertyfieldattribute of the Channel object to enter a channel and subscribe to presence events.

var realtime = new Ably.Realtime({
  key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA',
  clientId: 'bob' }
);
var channel = realtime.channels.get('cad-gun-bat');
channel.presence.subscribe('enter', function(member) {
  alert('Member ' + member.clientId + ' entered');
});
channel.presence.enter();
var Ably = require('ably');
var realtime = new Ably.Realtime({
  key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA',
  clientId: 'bob' }
);
var channel = realtime.channels.get('cad-gun-bat');
channel.presence.subscribe('enter', function(member) {
  console.log('Member ' + member.clientId + ' entered');
});
channel.presence.enter();
realtime = Ably::Realtime.new(key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA', client_id: 'bob')
channel = realtime.channels.get('cad-gun-bat')
channel.presence.subscribe(:enter) do |member|
  puts "Member #{member.client_id} entered"
end
channel.presence.enter
ClientOptions options = new ClientOptions("xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA");
options.clientId = "bob";
AblyRealtime realtime = new AblyRealtime(options);
Channel channel = realtime.channels.get("cad-gun-bat");
channel.presence.subscribe(new PresenceListener() {
  @Override
  public void onPresenceMessage(PresenceMessage member) {
    System.out.println("Member " + member.clientId + " : " + member.action.toString());
  }
});
channel.presence.enter(null, new CompletionListener());
ClientOptions options = new ClientOptions("xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA") { ClientId =  "bob"};
AblyRealtime realtime = new AblyRealtime(options);
IRealtimeChannel channel = realtime.Channels.Get("cad-gun-bat");
channel.Presence.Subscribe(member => {
  Console.WriteLine("Member " + member.ClientId + " : " + member.Action);
});
await channel.Presence.EnterAsync(null);
ARTClientOptions *options = [[ARTClientOptions alloc] initWithKey:@"xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA"];
options.clientId = @"bob";
ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options];
ARTRealtimeChannel *channel = [realtime.channels get:@"cad-gun-bat"];
[channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) {
    NSLog(@"Member %@ entered", member.clientId);
}];
[channel.presence enter:nil];
let options = ARTClientOptions(key: "xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA")
options.clientId = "bob"
let realtime = ARTRealtime(options: options)
let channel = realtime.channels.get("cad-gun-bat")
channel.presence.subscribe(.enter) { member in
    print("Member \(member.clientId) entered")
}
channel.presence.enter(nil)

Presence

In order to be present on a channel, a client must be identified by having a client ID, have permission to be present, and be attached to the channel. For simplicity, the library will implicitly attach to a channel when entering or subscribing to presence events. Clients are assigned a clientIdclient_idClientId when using token authentication, find out more about token authentication.

A single clientIdclient_idClientId may be present multiple times on the same channel via different client connections. As far as Ably is concerned, these are different members of the presence set for the channel, however they will be differentiated by their unique connectionIdConnectionIdconnection_id. 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 clientIdclient_idClientId and connectionIdConnectionIdconnection_id strings.

Presence states and 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. Subscribing to presence events makes it incredibly easy to build an app that shows, in real time, any changes to clients connected to Ably and present on a channel.

The following presence events are emitted:

:enterPresenceAction.ENTERAction.ENTERenter
A new member has entered the channel
:leavePresenceAction.LEAVEAction.LEAVEleave
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
:updatePresenceAction.UPDATEAction.UPDATEupdate
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
:presentPresenceAction.PRESENTAction.PRESENTpresent
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

View a presence states and events example

Member data

In addition to the clientIdclient_idClientId 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 :updateAction.UPDATEupdate event.

/* Subscribe to presence enter events */
channel.presence.on('enter', function(member) {
  console.log(member.data); // => not moving
});

/* Subscribe to presence update events */
channel.presence.on('update', function(member) {
  console.log(member.data); // => travelling North
});

/* Enter this client with data and update once entered */
channel.presence.enter('not moving', function(err) {
  channel.presence.update('travelling North');
});
/* Subscribe to presence enter events */
channel.presence.on('enter', function(member) {
  console.log(member.data); // => not moving
});

/* Subscribe to presence update events */
channel.presence.on('update', function(member) {
  console.log(member.data); // => travelling North
});

/* Enter this client with data and update once entered */
channel.presence.enter('not moving', function(err) {
  channel.presence.update('travelling North');
});
/* Subscribe to presence enter and update events */
channel.presence.subscribe(new PresenceListener() {
  @Override
  public void onPresenceMessage(PresenceMessage member) {
    switch (member.action) {
      case ENTER: {
        System.out.println(member.data); // => not moving
        break;
      }
      case UPDATE: {
        System.out.println(member.data); // => travelling North
        break;
      }
    }
  }
});

/* Enter this client with data and update once entered */
channel.presence.enter("not moving", new CompletionListener() {
  @Override
  public void onSuccess() {
    channel.presence.update("travelling North", new CompletionListener());
  }
});
/* Subscribe to presence enter and update events */
channel.Presence.Subscribe(member =>
{
    switch (member.Action)
    {
        case PresenceAction.Enter:
        case PresenceAction.Update:
            {
                Console.WriteLine(member.Data); // => travelling North
                break;
            }
    }
});

/* Enter this client with data and update once entered */
await channel.Presence.EnterAsync("not moving");
await channel.Presence.UpdateAsync("travelling North");
# Subscribe to presence enter events
channel.presence.subscribe(:enter) do |member|
  puts member.data # => not moving
end

# Subscribe to presence update events
channel.presence.subscribe(:update) do |member|
  puts member.data # => travelling North
end

# Enter this client with data and update once entered
channel.presence.enter(data: 'not moving') do
  channel.presence.update(data: 'travelling North')
end
// Subscribe to presence enter events
[channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) {
    NSLog(@"%@", member.data); // prints "not moving"
}];

// Subscribe to presence update events
[channel.presence subscribe:ARTPresenceUpdate callback:^(ARTPresenceMessage *member) {
    NSLog(@"%@", member.data); // prints "travelling North"
}];

// Enter this client with data and update once entered
[channel.presence enter:@"not moving" callback:^(ARTErrorInfo *error) {
    [channel.presence update:@"travelling North"];
}];
// Subscribe to presence enter events
channel.presence.subscribe(.enter) { member in
    print(member.data) // prints "not moving"
}

// Subscribe to presence update events
channel.presence.subscribe(.update) { member in
    print(member.data) // prints "travelling North"
}

// Enter this client with data and update once entered
channel.presence.enter("not moving") { error in
    channel.presence.update("travelling North")
}

Presence member list

The Presence object exposes a getGet method allowing a client to retrieve an array of all members present on the channel. The Ably client 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 attach and the presence set is updated on each subsequent presence event. Thus getGet returns the already-known presence set retained in memory and does not trigger a new request to the Ably service.

channel.presence.get(function(err, members) {
  console.log('There are ' + members.length + ' members on this channel');
  console.log('The first member has client ID: ' + members[0].clientId);
});
channel.presence.get(function(err, members) {
  console.log('There are ' + members.length + ' members on this channel');
  console.log('The first member has client ID: ' + members[0].clientId);
});
channel.presence.get do |members|
  puts "There are #{members.size} members on this channel"
  puts "The first member has client ID: #{members.first.client_id}"
end
PresenceMessage[] members = channel.presence.get();
System.out.println("There are " + members.length + " members on this channel");
System.out.println("The first member has client ID: " + members[0].clientId);
IEnumerable<PresenceMessage> presence = await channel.Presence.GetAsync();
Console.WriteLine($"There are {presence.Count()} members on this channel");
Console.WriteLine($"The first member has client ID: {presence.First().ClientId}");
[channel.presence get:^(NSArray<ARTPresenceMessage *> *members, ARTErrorInfo *error) {
    NSLog(@"There are %lu members on this channel", [members count]);
    NSLog(@"The first member has client ID: %@", members[0].clientId);
}];
channel.presence.get { members, error in
    print("There are \(members.count) members on this channel")
    print("The first member has client ID: \(members[0].clientId)")
}

Presence History

The Presence object exposes a historyHistory method allowing a client to retrieve historical presence events on the channel. Presence history can be used to return continuous presence event history up to the exact point a realtime channel was attached.

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 typically be stored for 24 – 72 hours. If persisted history is not enabled, Ably retains the last two minutes of presence event history in memory.

The following example retrieves the first two pages of historical presence events published up until the point the channel was attached.

channel.attach(function() {
  var presence = channel.presence;
  presence.history({ untilAttach: true }, function(err, resultPage) {
    if(err) {
      console.log('Unable to get presence history; err = ' + err.message);
    } else {
      console.log(resultPage.items.length + ' presence events received in first page');
      if(resultPage.hasNext()) {
        resultPage.next(function(err, nextPage) { ... });
      }
  });
});
channel.attach(function() {
  var presence = channel.presence;
  presence.history({ untilAttach: true }, function(err, resultPage) {
    if(err) {
      console.log('Unable to get presence history; err = ' + err.message);
    } else {
      console.log(resultPage.items.length + ' presence events received in first page');
      if(resultPage.hasNext()) {
        resultPage.next(function(err, nextPage) { ... });
      }
  });
});
channel.attach do
  presence = channel.presence
  presence.history(until_attach: true) do |result_page|
    puts "#{result_page.items.length} presence events received in first page"
    if result_page.has_next?
      result_page.next { |next_page| ... }
    end
  end
end
Param[] options = new Param[]{ new Param("untilAttach", "true") }
PaginatedResult<PresenceMessage> resultPage = channel.presence.history(options);
System.out.println(resultPage.items().length + " presence events received in first page");
if(resultPage.hasNext()) {
  PaginatedResult<PresenceMessage> nextPage = resultPage.next();
  System.out.println(nextPage.items().length + " presence events received in 2nd page");
}
PaginatedResult<PresenceMessage> resultPage;
resultPage = await channel.Presence.HistoryAsync(untilAttach: true);
Console.WriteLine(resultPage.Items.Count + " presence events received in first page");
if (resultPage.HasNext)
{
    PaginatedResult<PresenceMessage> nextPage = await resultPage.NextAsync();
    Console.WriteLine(nextPage.Items.Count + " presence events received in 2nd page");
}
ARTRealtimeHistoryQuery *query = [[ARTRealtimeHistoryQuery alloc] init];
query.untilAttach = YES;
[channel.presence history:query callback:^(ARTPaginatedResult<ARTPresenceMessage *> *resultPage,
                                           ARTErrorInfo *error) {
    NSLog(@"%lu presence events received in first page", [resultPage.items count]);
    if (resultPage.hasNext) {
        [resultPage next:^(ARTPaginatedResult<ARTPresenceMessage *> *nextPage, ARTErrorInfo *error) {
            NSLog(@"%lu presence events received in 2nd page", [nextPage.items count]);
        }];
    }
}];
let query = ARTRealtimeHistoryQuery()
query.untilAttach = true
channel.presence.history(query) { resultPage, error in
    let resultPage = resultPage!
    print("\(resultPage.items.count) presence events received in first page")
    if resultPage.hasNext {
        resultPage.next { nextPage, error in
            print("\(nextPage!.items.count) presence events received in 2nd page")
        }
    }
}

See the presence history documentation for further details of the supported query parameters.

Managing multiple client IDs

Each unique clientIdclient_idClientId 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 clientIdclient_idClientId and shared connectionIdConnectionIdconnection_id

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 clientIdclient_idClientId specified at the time of the call, rather than implicitly based on the clientIdclient_idClientId specified when the library is instantiated or authenticated.

In order to be able to publish presence changes for arbitrary client IDs, the client library must have been instantiated either with an API key, or with a token bound to a wildcard client ID.

var rest = new Ably.Rest({ key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA' });
/* request a wildcard token */
rest.auth.requestToken({ clientId: '*' }, function(err, token) {
  var realtime = new Ably.Realtime({ token: token });
  var channel = realtime.channels.get('realtime-chat');

  channel.presence.subscribe('enter', function(member) {
    console.log(member.client_id + 'entered realtime-chat');
  });

  channel.presence.enterClient('Bob'); // => Bob entered realtime-chat
  channel.presence.enterClient('Mary'); // => Mary entered realtime-chat
});
var rest = new Ably.Rest({ key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA' });
/* request a wildcard token */
rest.auth.requestToken({ clientId: '*' }, function(err, token) {
  var realtime = new Ably.Realtime({ token: token });
  var channel = realtime.channels.get('realtime-chat');

  channel.presence.subscribe('enter', function(member) {
    console.log(member.client_id + 'entered realtime-chat');
  });

  channel.presence.enterClient('Bob'); // => Bob entered realtime-chat
  channel.presence.enterClient('Mary'); // => Mary entered realtime-chat
});
rest = Ably::Rest.new(key: 'xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA')
# request a wildcard token
rest.auth.requestToken(clientId: '*') do |token|
  realtime = Ably::Realtime.new(token: token)
  channel = realtime.channels.get('realtime-chat')

  channel.presence.subscribe(:enter) do |member|
    puts "#{member.client_id} entered realtime-chat"
  end

  channel.presence.enter_client 'Bob' # => Bob entered realtime-chat
  channel.presence.enter_client 'Mary' # => Mary entered realtime-chat
end
/* request a wildcard token */
AblyRest rest = new AblyRest('xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA');
TokenParams params = new TokenParams();
params.clientId = "*";
ClientOptions options = new ClientOptions();
options.tokenDetails = rest.auth.requestToken(params, null);

AblyRealtime realtime = new AblyRealtime(options);
Channel channel = realtime.channels.get("realtime-chat");

channel.presence.subscribe(new PresenceListener() {
  @Override
  public void onPresenceMessage(PresenceMessage member) {
    System.out.println(member.clientId + " entered realtime-chat");
  }
});

CompletionListener noop = new CompletionListener();
channel.presence.enterClient("Bob", noop); /* => Bob entered realtime-chat */
channel.presence.enterClient('Mary', noop); /* => Mary entered realtime-chat */
/* request a wildcard token */
AblyRest rest = new AblyRest("xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA");
TokenParams tokenParams = new TokenParams() { ClientId = "*"};
ClientOptions options = new ClientOptions();
options.TokenDetails = await rest.Auth.RequestTokenAsync(tokenParams, null);

AblyRealtime realtime = new AblyRealtime(options);
IRealtimeChannel channel = realtime.Channels.Get("realtime-chat");

channel.Presence.Subscribe(member => {
  Console.WriteLine(member.ClientId + " entered realtime-chat");
});

await channel.Presence.EnterClientAsync("Bob", null); /* => Bob entered realtime-chat */
await channel.Presence.EnterClientAsync("Mary", null); /* => Mary entered realtime-chat */
ARTRest* rest = [[ARTRest alloc] initWithKey:@"xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA"];
// request a wildcard token
ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"*"];
[rest.auth requestToken:tokenParams withOptions:nil callback:^(ARTTokenDetails *tokenDetails,
                                                              NSError *error) {
    ARTRealtime *realtime = [[ARTRealtime alloc] initWithToken:tokenDetails.token];
    ARTRealtimeChannel *channel = [realtime.channels get:@"realtime-chat"];

    [channel.presence subscribe:ARTPresenceEnter callback:^(ARTPresenceMessage *member) {
        NSLog(@"%@ entered realtime-chat", member.clientId);
    }];

    [channel.presence enterClient:@"Bob" data:nil]; // prints 'Bob entered realtime-chat'
    [channel.presence enterClient:@"Mary" data:nil]; // prints 'Mary entered realtime-chat'
}];
let rest = ARTRest(key: "xVLyHw.XVJnXQ:a0rdM9T6aDq2-U9BNZInEO3LLWDLtM7dM3ncSKaPuDA")
// request a wildcard token
rest.auth.requestToken(ARTTokenParams(clientId: "*"), withOptions: nil) { tokenDetails, error in
    let realtime = ARTRealtime(token: tokenDetails!.token)
    let channel = realtime.channels.get("realtime-chat")

    channel.presence.subscribe(.enter) { member in
        print("\(member.clientId) entered realtime-chat")
    }

    channel.presence.enterClient("Bob", data: nil) // prints 'Bob entered realtime-chat'
    channel.presence.enterClient("Mary", data: nil) // prints 'Mary entered realtime-chat'
}

API Reference

View the Presence API Reference.


Need help?

If you need any help with your implementation or if you have encountered any problems, do get in touch. You can also quickly find answers from our knowledge base, and blog.