ably

ably /ably-js

Javascript, Node, Typescript, React, React Native client library SDK for Ably realtime messaging service

335
59

Repository Statistics

Key metrics and engagement data

335
Stars
59
Forks
179
Open Issues
0
Releases
1.18
Engagement Rate
Default branch: main

Timeline

Repository has been active for 12 years, 9 months

Repository Created

Last Commit
Recently active

README.md

Ably

Features

Ably is the platform that powers synchronized digital experiences in realtime. Whether attending an event in a virtual venue, receiving realtime financial information, or monitoring live car performance data – consumers simply expect realtime digital experiences as standard. Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime for more than 250 million devices across 80 countries each month. Organizations like Bloomberg, HubSpot, Verizon, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale. For more information, see the Ably documentation.

npm version

This is a JavaScript client library for Ably Realtime.

This library currently targets the Ably client library features spec Version 1.2. You can jump to the 'Known Limitations' section to see the features this client library does not yet support or view our client library SDKs feature support matrix to see the list of all the available features.

Supported platforms

This SDK supports the following platforms:

Browsers: All major desktop and mobile browsers, including (but not limited to) Chrome, Firefox, Edge, Safari on iOS and macOS, Opera, and Android browsers. IE is not supported. See compatibility table below for more information on minimum supported versions for major browsers:

BrowserMinimum supported versionRelease date
Chrome58Apr 19, 2017
Firefox52Mar 7, 2017
Edge79Dec 15, 2020
Safari11Sep 19, 2017
Opera45May 10, 2017

Webpack: see using Webpack in browsers, or our guide for serverside Webpack

Node.js: version 16.x or newer. (1.1.x versions work on Node.js 4.5 or newer, 1.2.x versions work on Node.js 8.17 or newer). We do not currently provide an ESM bundle, please contact us if you would would like to use ably-js in a NodeJS ESM project.

React: We offer a set of React Hooks which make it seamless to use ably-js in your React application. See the React Hooks documentation for more details.

React Native: We aim to support all platforms supported by React Native. If you find any issues please raise an issue or contact us.

NativeScript: see ably-js-nativescript

TypeScript: see below

WebWorkers: The browser bundle supports running in a Web Worker context. You can also use the modular variant of the library in Web Workers.

We test the library against a selection of browsers using their latest versions. Please refer to the test-browser GitHub workflow for the set of browsers that currently undergo CI testing.

We regression-test the library against a selection of Node.js versions, which will change over time. We will always support and test against current LTS Node.js versions, and optionally some older versions that are still supported by upstream dependencies. We reserve the right to drop support for non-LTS versions in a non-major release. We will update the engines field in package.json whenever we change the Node.js versions supported by the project. Please refer to the test-node GitHub workflow for the set of versions that currently undergo CI testing.

However, we aim to be compatible with a much wider set of platforms and browsers than we can possibly test on. That means we'll happily support (and investigate reported problems with) any reasonably-widely-used browser. So if you find any compatibility issues, please do raise an issue in this repository or contact Ably customer support for advice.

If you require support for older browsers and Node.js, you can use the security-maintained version 1 of the library. Install version 1 via CDN link, or from npm with npm install ably@1 --save. It supports IE versions 9 or newer, older versions of major browsers, and Node.js 8.17 or newer. Note that version 1 will only receive security updates and critical bug fixes, and won't include any new features.

For complete API documentation, see the Ably documentation.

Installation

Node.js

1npm install ably --save

and require as:

javascript
1var Ably = require('ably');

For usage, jump to Using the Realtime API or Using the REST API.

Serverside usage with Webpack

If you are using a version older than 1.2.5 you will need to add 'ably' to externals in your Webpack config to exclude it from Webpack processing, and require and use it in as a external module using require('ably') as above.

For browsers

Include the Ably library in your HTML:

html
1<script src="https://cdn.ably.com/lib/ably.min-2.js"></script>

The Ably client library follows Semantic Versioning. To lock into a major or minor version of the client library, you can specify a specific version number such as https://cdn.ably.com/lib/ably.min-2.js for all v2._ versions, or https://cdn.ably.com/lib/ably.min-2.6.js for all v2.6._ versions, or you can lock into a single release with https://cdn.ably.com/lib/ably.min-2.6.3.js. Note you can load the non-minified version by omitting .min from the URL such as https://cdn.ably.com/lib/ably-2.js. See https://github.com/ably/ably-js/tags for a list of tagged releases.

For usage, jump to Using the Realtime API or Using the REST API.

Using Webpack

(This applies to using Webpack to compile for a browser; for Node.js, see Serverside usage with Webpack)

Webpack will search your node_modules folder by default, so if you include ably in your package.json file, when running Webpack the following will allow you to require('ably') (or if using typescript or ES6 modules, import * as Ably from 'ably';). If your Webpack target is set to 'browser', this will automatically use the browser commonjs distribution.

If that doesn't work for some reason (e.g. you are using a custom Webpack target), you can use one of the solutions below depending on your Webpack version:

  • for Webpack 5: you can use alias setting in the Webpack config like this:

    javascript
    1// webpack.config.js
    2const path = require('path');
    3
    4module.exports = {
    5 module: {
    6 rules: [
    7 {
    8 resolve: {
    9 alias: {
    10 ably: path.resolve(__dirname, 'node_modules/ably/build/ably.js'),
    11 },
    12 },
    13 },
    14 ],
    15 },
    16};
  • for Webpack before 5: you can reference the ably.js static file directly: require('ably/build/ably.js'); (or import * as Ably from 'ably/build/ably.js' for typescript / ES6 modules).

Modular (tree-shakable) variant

Aimed at those who are concerned about their app’s bundle size, the modular variant of the library allows you to create a client which has only the functionality that you choose. Unused functionality can then be tree-shaken by your module bundler.

The modular variant of the library provides:

  • a BaseRealtime class;
  • various plugins that add functionality to a BaseRealtime instance, such as Rest, RealtimePresence, etc.

To use this variant of the library, import the BaseRealtime class from ably/modular, along with the plugins that you wish to use. Then, pass these plugins to the BaseRealtime constructor as shown in the example below:

javascript
1import { BaseRealtime, WebSocketTransport, FetchRequest, RealtimePresence } from 'ably/modular';
2
3const client = new BaseRealtime({
4 key: 'YOUR_ABLY_API_KEY' /* Replace with a real key from the Ably dashboard */,
5 plugins: {
6 WebSocketTransport,
7 FetchRequest,
8 RealtimePresence,
9 },
10});

You must provide:

  • at least one HTTP request implementation; that is, one of FetchRequest or XHRRequest;
  • at least one realtime transport implementation; that is, one of WebSocketTransport or XHRPolling.

BaseRealtime offers the same API as the Realtime class described in the rest of this README. This means that you can develop an application using the default variant of the SDK and switch to the modular version when you wish to optimize your bundle size.

In order to further reduce bundle size, the modular variant of the SDK performs less logging than the default variant. It only logs:

  • messages that have a logLevel of 1 (that is, errors)
  • a small number of other network events

If you need more verbose logging, use the default variant of the SDK.

For more information about the modular variant of the SDK, see the generated documentation (this link points to the documentation for the main branch).

TypeScript

The TypeScript typings are included in the package and so all you have to do is:

typescript
1import * as Ably from 'ably';
2
3let options: Ably.ClientOptions = { key: 'foo' };
4let client = new Ably.Realtime(options); /* inferred type Ably.Realtime */
5let channel = client.channels.get('feed'); /* inferred type Ably.RealtimeChannel */

Intellisense in IDEs with TypeScript support is supported:

TypeScript suggestions

If you need to explicitly import the type definitions, see ably.d.ts.

NativeScript

See the ably-js-nativescript repo for NativeScript usage details.

Using the Realtime API

This readme gives some basic examples; for our full API documentation, please go to https://www.ably.com/docs .

Introduction

All examples assume a client has been created as follows:

ts
1// basic auth with an API key
2var client = new Ably.Realtime(key: string);
3
4// using a Client Options object, see https://www.ably.com/docs/rest/usage#client-options
5// which must contain at least one auth option, i.e. at least
6// one of: key, token, tokenDetails, authUrl, or authCallback
7var client = new Ably.Realtime(options: ClientOptions);

Connection

Successful connection:

javascript
1client.connection.on('connected', function() {
2 # successful connection
3});

Failed connection:

javascript
1client.connection.on('failed', function() {
2 # failed connection
3});

Subscribing to a channel

Given:

javascript
1var channel = client.channels.get('test');

Subscribe to all events:

javascript
1channel.subscribe(function (message) {
2 message.name; // 'greeting'
3 message.data; // 'Hello World!'
4});

Only certain events:

javascript
1channel.subscribe('myEvent', function (message) {
2 message.name; // 'myEvent'
3 message.data; // 'myData'
4});

Subscribing to a channel with deltas

Subscribing to a channel in delta mode enables delta compression. This is a way for a client to subscribe to a channel so that message payloads sent contain only the difference (ie the delta) between the present message and the previous message on the channel.

Configuring a channel for deltas is detailed in the @ably-forks/vcdiff-decoder documentation.

Beyond specifying channel options, the rest is transparent and requires no further changes to your application. The message.data instances that are delivered to your listening function continue to contain the values that were originally published.

If you would like to inspect the Message instances in order to identify whether the data they present was rendered from a delta message from Ably then you can see if extras.delta.format equals 'vcdiff'.

Publishing to a channel

javascript
1// Publish a single message with name and data
2await channel.publish('greeting', 'Hello World!');
3
4// Publish several messages at once
5await channel.publish([{name: 'greeting', data: 'Hello World!'}, ...]);

Querying the History

javascript
1const messagesPage = channel.history()
2messagesPage // PaginatedResult
3messagesPage.items // array of Message
4messagesPage.items[0].data // payload for first message
5messagesPage.items.length // number of messages in the current page of history
6messagesPage.hasNext() // true if there are further pages
7messagesPage.isLast() // true if this page is the last page
8const nextPage = await messagesPage.next(); // retrieves the next page as PaginatedResult
9
10// Can optionally take an options param, see https://www.ably.com/docs/rest-api/#message-history
11const messagesPage = await channel.history({start: ..., end: ..., limit: ..., direction: ...});

Presence on a channel

Getting presence:

javascript
1const presenceSet = channel.presence.get();
2presenceSet; // array of PresenceMessages

Note that presence#get on a realtime channel does not return a PaginatedResult, as the library maintains a local copy of the presence set.

Entering (and leaving) the presence set:

javascript
1await channel.presence.enter('my status');
2// now I am entered
3
4await channel.presence.update('new status');
5// my presence data is updated
6
7await channel.presence.leave();
8// I've left the presence set

If you are using a client which is allowed to use any clientId -- that is, if you didn't specify a clientId when initializing the client, and are using basic auth or a token witha wildcard clientId (see https://www.ably.com/docs/general/authentication for more information), you can use

javascript
1await channel.presence.enterClient('myClientId', 'status');
2// and similarly, updateClient and leaveClient

Querying the Presence History

javascript
1const messagesPage = channel.presence.history(); // PaginatedResult
2messagesPage.items // array of PresenceMessage
3messagesPage.items[0].data // payload for first message
4messagesPage.items.length // number of messages in the current page of history
5messagesPage.hasNext() // true if there are further pages
6messagesPage.isLast() // true if this page is the last page
7const nextPage = await messagesPage.next(); // retrieves the next page as PaginatedResult
8
9// Can optionally take an options param, see https://www.ably.com/docs/rest-api/#message-history
10const messagesPage = await channel.presence.history({start: ..., end: ..., limit: ..., direction: ...);

Symmetrical end-to-end encrypted payloads on a channel

When a 128 bit or 256 bit key is provided to the library, the data attributes of all messages are encrypted and decrypted automatically using that key. The secret key is never transmitted to Ably. See https://www.ably.com/docs/realtime/encryption

javascript
1// Generate a random 256-bit key for demonstration purposes (in
2// practice you need to create one and distribute it to clients yourselves)
3const key = await Ably.Realtime.Crypto.generateRandomKey();
4var channel = client.channels.get('channelName', { cipher: { key: key } });
5
6channel.subscribe(function (message) {
7 message.name; // 'name is not encrypted'
8 message.data; // 'sensitive data is encrypted'
9});
10
11channel.publish('name is not encrypted', 'sensitive data is encrypted');

You can also change the key on an existing channel using setOptions (which completes after the new encryption settings have taken effect):

javascript
1await channel.setOptions({cipher: {key: <key>}});
2// New encryption settings are in effect

Message interactions

Message Interactions allow you to interact with messages previously sent to a channel. Once a channel is enabled with Message Interactions, messages received by that channel will contain a unique timeSerial that can be referenced by later messages.

Example emoji reaction to a message:

javascript
1function sendReaction(emoji) {
2 channel.publish({
3 name: 'event_name',
4 data: emoji,
5 extras: { ref: { type: 'com.ably.reaction', timeserial: '1656424960320-1' } },
6 });
7}

See https://www.ably.com/docs/realtime/messages#message-interactions for more detail.

Fallback transport mechanisms

Ably-js has fallback transport mechanisms to ensure its realtime capabilities can function in network conditions (such as firewalls or proxies) that might prevent the client from establishing a WebSocket connection.

The default Ably.Realtime client includes these mechanisms by default. If you are using modular variant of the library, you may wish to provide the BaseRealtime instance with one or more alternative transport modules, namely XHRStreaming and/or XHRPolling, alongside WebSocketTransport, so your connection is less susceptible to these external conditions. For instructions on how to do this, refer to the modular variant of the library section.

Each of these fallback transport mechanisms is supported and tested on all the browsers we test against, even when those browsers do not themselves require those fallbacks.

Using the REST API

This readme gives some basic examples. For our full API documentation, please go to https://www.ably.com/docs .

Introduction

All examples assume a client and/or channel has been created as follows:

ts
1// basic auth with an API key
2var client = new Ably.Rest(key: string);
3
4// using a Client Options object, see https://www.ably.com/docs/realtime/usage#client-options
5// which must contain at least one auth option, i.e. at least
6// one of: key, token, tokenDetails, authUrl, or authCallback
7var client = new Ably.Rest(options: ClientOptions);

Given:

javascript
1var channel = client.channels.get('test');

Publishing to a channel

javascript
1// Publish a single message with name and data
2try {
3 channel.publish('greeting', 'Hello World!');
4 console.log('publish succeeded');
5} catch (err) {
6 console.log('publish failed with error ' + err);
7}
8
9// Publish several messages at once
10await channel.publish([{name: 'greeting', data: 'Hello World!'}, ...]);

Querying the History

javascript
1const messagesPage = await channel.history();
2messagesPage // PaginatedResult
3messagesPage.items // array of Message
4messagesPage.items[0].data // payload for first message
5messagesPage.items.length // number of messages in the current page of history
6messagesPage.hasNext() // true if there are further pages
7messagesPage.isLast() // true if this page is the last page
8const nextPage = await messagesPage.next(); // retrieves the next page as PaginatedResult
9
10// Can optionally take an options param, see https://www.ably.com/docs/rest-api/#message-history
11await channel.history({start: ..., end: ..., limit: ..., direction: ...});

Presence on a channel

javascript
1const presencePage = await channel.presence.get(); // PaginatedResult
2presencePage.items; // array of PresenceMessage
3presencePage.items[0].data; // payload for first message
4presencePage.items.length; // number of messages in the current page of members
5presencePage.hasNext(); // true if there are further pages
6presencePage.isLast(); // true if this page is the last page
7const nextPage = await presencePage.next(); // retrieves the next page as PaginatedResult

Querying the Presence History

javascript
1const messagesPage = channel.presence.history(); // PaginatedResult
2messagesPage.items // array of PresenceMessage
3messagesPage.items[0].data // payload for first message
4messagesPage.items.length // number of messages in the current page of history
5messagesPage.hasNext() // true if there are further pages
6messagesPage.isLast() // true if this page is the last page
7const nextPage = await messagesPage.next(); // retrieves the next page as PaginatedResult
8
9// Can optionally take an options param, see https://www.ably.com/docs/rest-api/#message-history
10const messagesPage = channel.history({start: ..., end: ..., limit: ..., direction: ...});

Getting the status of a channel

javascript
1const channelDetails = await channel.status();
2channelDetails.channelId; // The name of the channel
3channelDetails.status.isActive; // A boolean indicating whether the channel is active
4channelDetails.status.occupancy; // Contains metadata relating to the occupants of the channel

Generate Token and Token Request

See https://www.ably.com/docs/general/authentication for an explanation of Ably's authentication mechanism.

Requesting a token:

javascript
1const tokenDetails = await client.auth.requestToken();
2// tokenDetails is instance of TokenDetails
3// see https://www.ably.com/docs/rest/authentication/#token-details for its properties
4
5// Now we have the token, we can send it to someone who can instantiate a client with it:
6var clientUsingToken = new Ably.Realtime(tokenDetails.token);
7
8// requestToken can take two optional params
9// tokenParams: https://www.ably.com/docs/rest/authentication/#token-params
10// authOptions: https://www.ably.com/docs/rest/authentication/#auth-options
11const tokenDetails = await client.auth.requestToken(tokenParams, authOptions);

Creating a token request (for example, on a server in response to a request by a client using the authCallback or authUrl mechanisms):

javascript
1const tokenRequest = await client.auth.createTokenRequest();
2// now send the tokenRequest back to the client, which will
3// use it to request a token and connect to Ably
4
5// createTokenRequest can take two optional params
6// tokenParams: https://www.ably.com/docs/rest/authentication/#token-params
7// authOptions: https://www.ably.com/docs/rest/authentication/#auth-options
8const tokenRequest = await client.auth.createTokenRequest(tokenParams, authOptions);

Fetching your application's stats

javascript
1const statsPage = await client.stats(); // statsPage as PaginatedResult
2statsPage.items; // array of Stats
3statsPage.items[0].inbound.rest.messages.count; // total messages published over REST
4statsPage.items.length; // number of stats in the current page of history
5statsPage.hasNext(); // true if there are further pages
6statsPage.isLast(); // true if this page is the last page
7const nextPage = await statsPage.next(); // retrieves the next page as PaginatedResult

Fetching the Ably service time

javascript
1const time = await client.time(); // time is in ms since epoch

Push activation

Push activation is supported for browser clients, via the Push plugin. In order to use push activation, you must pass in the plugin via client options.

You also need to provide a path to a service worker which will be registered when the client is activated, and will handle receipt of push notifications.

javascript
1import * as Ably from 'ably';
2import Push from 'ably/push';
3
4const client = new Ably.Rest({
5 ...options,
6 pushServiceWorkerUrl: '/my_service_worker.js',
7 plugins: { Push },
8});

Example service worker:

javascript
1// my_service_worker.js
2self.addEventListener('push', async (event) => {
3 const { notification } = event.data.json();
4 self.registration.showNotification(notification.title, notification);
5});

To register the device to receive push notifications, you must call the activate method:

javascript
1await client.push.activate();

Once the client is activated, you can subscribe to receive push notifcations on a channel:

javascript
1const channel = client.channels.get('my_push_channel');
2
3// Subscribe the device to receive push notifcations for a channel...
4await channel.push.subscribeDevice();
5
6// ...or subscribe all devices associated with the client's cliendId to receive notifcations from the channel
7await channel.push.subscribeClient();
8
9// When you no longer need to be subscribed to push notifcations, you can remove the subscription:
10await channel.push.unsubscribeDevice();
11// Or:
12await channel.push.unsubscribeClient();

Push activation works with the Modular variant of the library, but requires you to be using the Rest plugin.

Alternatively, you can load the Push plugin directly in your HTML using script tag (in case you can't use a package manager):

html
1<script src="https://cdn.ably.com/lib/push.umd.min-2.js"></script>

When loaded this way, the Push plugin will be available on the global object via the AblyPushPlugin property, so you will need to pass it to the Ably instance as follows:

javascript
1const client = new Ably.Rest({
2 ...options,
3 plugins: { Push: AblyPushPlugin },
4});

The Push plugin is developed as part of the Ably client library, so it is available for the same versions as the Ably client library itself. It also means that it follows the same semantic versioning rules as they were defined for the Ably client library. For example, to lock into a major or minor version of the Push plugin, you can specify a specific version number such as https://cdn.ably.com/lib/push.umd.min-2.js for all v2._ versions, or https://cdn.ably.com/lib/push.umd.min-2.4.js for all v2.4._ versions, or you can lock into a single release with https://cdn.ably.com/lib/push.umd.min-2.4.0.js. Note you can load the non-minified version by omitting .min from the URL such as https://cdn.ably.com/lib/push.umd-2.js.

For more information on publishing push notifications over Ably, see the Ably push documentation.

LiveObjects

Using the Objects plugin

LiveObjects functionality is supported for Realtime clients via the Objects plugin. In order to use Objects on a channel, you must pass in the plugin via client options.

typescript
1import * as Ably from 'ably';
2import Objects from 'ably/objects';
3
4const client = new Ably.Realtime({
5 ...options,
6 plugins: { Objects },
7});

Objects plugin also works with the Modular variant of the library.

Alternatively, you can load the Objects plugin directly in your HTML using script tag (in case you can't use a package manager):

html
1<script src="https://cdn.ably.com/lib/objects.umd.min-2.js"></script>

When loaded this way, the Objects plugin will be available on the global object via the AblyObjectsPlugin property, so you will need to pass it to the Ably instance as follows:

typescript
1const client = new Ably.Realtime({
2 ...options,
3 plugins: { Objects: AblyObjectsPlugin },
4});

The Objects plugin is developed as part of the Ably client library, so it is available for the same versions as the Ably client library itself. It also means that it follows the same semantic versioning rules as they were defined for the Ably client library. For example, to lock into a major or minor version of the Objects plugin, you can specify a specific version number such as https://cdn.ably.com/lib/objects.umd.min-2.js for all v2._ versions, or https://cdn.ably.com/lib/objects.umd.min-2.4.js for all v2.4._ versions, or you can lock into a single release with https://cdn.ably.com/lib/objects.umd.min-2.4.0.js. Note you can load the non-minified version by omitting .min from the URL such as https://cdn.ably.com/lib/objects.umd-2.js.

For more information about the LiveObjects product, see the Ably LiveObjects documentation.

Objects Channel Modes

To use the Objects on a channel, clients must attach to a channel with the correct channel mode:

  • object_subscribe - required to retrieve Objects for a channel
  • object_publish - required to create new and modify existing Objects on a channel
typescript
1const client = new Ably.Realtime({
2 // authentication options
3 ...options,
4 plugins: { Objects },
5});
6const channelOptions = { modes: ['object_subscribe', 'object_publish'] };
7const channel = client.channels.get('my_objects_channel', channelOptions);
8const objects = channel.objects;

The authentication token must include corresponding capabilities for the client to interact with Objects.

Getting the Root Object

The root object represents the top-level entry point for objects within a channel. It gives access to all other nested objects.

typescript
1const root = await objects.getRoot();

The root object is a LiveMap instance and serves as the starting point for storing and organizing Objects on a channel.

Object Types

LiveObjects currently supports two primary data structures; LiveMap and LiveCounter.

LiveMap - A key/value map data structure, similar to a JavaScript Map, where all changes are synchronized across clients in realtime. It enables you to store primitive values and other objects, enabling composability.

You can use LiveMap as follows:

typescript
1// root object is a LiveMap
2const root = await objects.getRoot();
3
4// you can read values for a key with .get
5root.get('foo');
6root.get('bar');
7
8// get a number of key/value pairs in a map with .size
9root.size();
10
11// iterate over keys/values in a map
12for (const [key, value] of root.entries()) {
13 /**/
14}
15for (const key of root.keys()) {
16 /**/
17}
18for (const value of root.values()) {
19 /**/
20}
21
22// set keys on a map with .set
23// different data types are supported
24await root.set('foo', 'Alice');
25await root.set('bar', 1);
26await root.set('baz', true);
27await root.set('qux', new Uint8Array([21, 31]));
28// as well as other objects
29const counter = await objects.createCounter();
30await root.set('quux', counter);
31
32// and you can remove keys with .remove
33await root.remove('name');

LiveCounter - A counter that can be incremented or decremented and is synchronized across clients in realtime

You can use LiveCounter as follows:

typescript
1const counter = await objects.createCounter();
2
3// you can get current value of a counter with .value
4counter.value();
5
6// and change its value with .increment or .decrement
7await counter.increment(5);
8await counter.decrement(2);

Subscribing to Updates

Subscribing to updates on objects enables you to receive changes made by other clients in realtime. Since multiple clients may modify the same objects, subscribing ensures that your application reacts to external updates as soon as they are received.

Additionally, mutation methods such as LiveMap.set, LiveCounter.increment, and LiveCounter.decrement do not directly edit the current state of the object locally. Instead, they send the intended operation to the Ably system, and the change is applied to the local object only when the corresponding realtime operation is echoed back to the client. This means that the state you retrieve immediately after a mutation may not reflect the latest updates yet.

You can subscribe to updates on all objects using subscription listeners as follows:

typescript
1const root = await objects.getRoot();
2
3// subscribe to updates on a LiveMap
4root.subscribe((update: LiveMapUpdate) => {
5 console.log('LiveMap "name" key:', root.get('name')); // can read the current value for a key in a map inside this callback
6 console.log('LiveMap update details:', update); // and can get update details from the provided update object
7});
8
9// subscribe to updates on a LiveCounter
10const counter = await objects.createCounter();
11counter.subscribe((update: LiveCounterUpdate) => {
12 console.log('LiveCounter new value:', counter.value()); // can read the current value of the counter inside this callback
13 console.log('LiveCounter update details:', update); // and can get update details from the provided update object
14});
15
16// perform operations on LiveMap and LiveCounter
17await root.set('name', 'Alice');
18// LiveMap "name" key: Alice
19// LiveMap update details: { update: { name: 'updated' } }
20
21await root.remove('name');
22// LiveMap "name" key: undefined
23// LiveMap update details: { update: { name: 'removed' } }
24
25await counter.increment(5);
26// LiveCounter new value: 5
27// LiveCounter update details: { update: { amount: 5 } }
28
29await counter.decrement(2);
30// LiveCounter new value: 3
31// LiveCounter update details: { update: { amount: -2 } }

You can deregister subscription listeners as follows:

typescript
1// use dedicated unsubscribe function from the .subscribe call
2const { unsubscribe } = root.subscribe(() => {});
3unsubscribe();
4
5// call .unsubscribe with a listener reference
6const listener = () => {};
7root.subscribe(listener);
8root.unsubscribe(listener);
9
10// deregister all listeners using .unsubscribeAll
11root.unsubscribeAll();

Creating New Objects

New LiveMap and LiveCounter objects can be created as follows:

typescript
1const counter = await objects.createCounter(123); // with optional initial counter value
2const map = await objects.createMap({ key: 'value' }); // with optional initial map entries

To persist them on a channel and share them between clients, they must be assigned to a parent LiveMap that is connected to the root object through the object hierarchy:

typescript
1const root = await objects.getRoot();
2
3const counter = await objects.createCounter();
4const map = await objects.createMap({ counter });
5const outerMap = await objects.createMap({ map });
6
7await root.set('outerMap', outerMap);
8
9// resulting structure:
10// root (LiveMap)
11// └── outerMap (LiveMap)
12// └── map (LiveMap)
13// └── counter (LiveCounter)

Batch Operations

Batching enables multiple operations to be grouped into a single channel message that is sent to the Ably service. This guarantees that all changes are applied atomically.

Within a batch callback, the BatchContext instance provides wrapper objects around regular LiveMap and LiveCounter objects with a synchronous API for storing changes in the batch context.

typescript
1await objects.batch((ctx) => {
2 const root = ctx.getRoot();
3
4 root.set('foo', 'bar');
5 root.set('baz', 42);
6
7 const counter = root.get('counter');
8 counter.increment(5);
9
10 // batched operations are sent to the Ably service when the batch callback returns
11});

Lifecycle Events

LiveObjects emit events that allow you to monitor objects' lifecycle changes, such as synchronization progress and object deletions.

Synchronization Events - the syncing and synced events notify when the local Objects state on a client is being synchronized with the Ably service. These events can be useful for displaying loading indicators, preventing user edits during synchronization, or triggering application logic when the data was loaded for the first time.

typescript
1objects.on('syncing', () => {
2 console.log('Objects are syncing...');
3 // Example: Show a loading indicator
4});
5
6objects.on('synced', () => {
7 console.log('Objects have been synced.');
8 // Example: Hide loading indicator
9});

Object Deletion Events - objects that have been orphaned for a long period (i.e., not connected to the object tree by being set as a key in a map accessible from the root map object) will eventually be deleted. Once an object is deleted, it can no longer be interacted with. You should avoid accessing its data or trying to update its value and you should remove all references to the deleted object in your application.

typescript
1const root = await objects.getRoot();
2const counter = root.get('counter');
3
4counter.on('deleted', () => {
5 console.log('Object has been deleted.');
6 // Example: Remove references to the object from the application
7});

To unsubscribe from lifecycle events:

typescript
1// same API for channel.objects and LiveObject instances
2// use dedicated off function from the .on call
3const { off } = objects.on('synced', () => {});
4off();
5
6// call .off with an event name and a listener reference
7const listener = () => {};
8objects.on('synced', listener);
9objects.off('synced', listener);
10
11// deregister all listeners using .offAll
12objects.offAll();

Typing Objects

You can provide your own TypeScript typings for Objects by providing a globally defined AblyObjectsTypes interface.

typescript
1// file: ably.config.d.ts
2import { LiveCounter, LiveMap } from 'ably';
3
4type MyCustomRoot = {
5 map: LiveMap<{
6 foo: string;
7 counter: LiveCounter;
8 }>;
9};
10
11declare global {
12 export interface AblyObjectsTypes {
13 root: MyCustomRoot;
14 }
15}

Note that using TypeScript typings for Objects does not provide runtime type checking; instead, it enables code completion and editor hints (if supported by your IDE) when interacting with the Objects API:

typescript
1const root = await objects.getRoot(); // uses types defined by global AblyObjectsTypes interface by default
2
3const map = root.get('map'); // LiveMap<{ foo: string; counter: LiveCounter }>
4map.set('foo', 1); // TypeError
5map.get('counter').value(); // autocompletion for counter method names

You can also provide typings for the channel Objects when calling the objects.getRoot method, allowing you to have different typings for different channels:

typescript
1type ReactionsRoot = {
2 hearts: LiveCounter;
3 likes: LiveCounter;
4};
5
6type PollsRoot = {
7 currentPoll: LiveMap;
8};
9
10const reactionsRoot = await reactionsChannel.objects.getRoot<ReactionsRoot>();
11const pollsRoot = await pollsChannel.objects.getRoot<PollsRoot>();

Delta Plugin

From version 1.2 this client library supports subscription to a stream of Vcdiff formatted delta messages from the Ably service. For certain applications this can bring significant data efficiency savings. This is an optional feature so our

See the @ably-forks/vcdiff-decoder documentation for setup and usage examples.

Support, feedback and troubleshooting

Please visit http://support.ably.com/ for access to our knowledgebase and to ask for any assistance.

You can also view the community reported Github issues.

To see what has changed in recent versions, see the CHANGELOG.

Browser-specific issues

Chrome Extensions

ably-js works out-of-the-box in background scripts for Chrome extensions using manifest v2. However, since manifest v3 background pages are no longer supported so you will need to run ably-js inside a service worker. If you are using an ably-js realtime client in a service worker, note that in versions of Chrome before 116, active WebSockets would not reset the 30s service worker idle timer, resulting in the client being closed prematurely, however, in versions 116 and above, service workers will stay active as long as a client is connected. You can ensure that your extension only runs in versions 116 and above by adding the following to your manifest.json:

json
1{
2 ...
3 "minimum_chrome_version": "116",
4 ...
5}

Next.js with App Router and Turbopack

If you are using ably-js in your Next.js project with App Router and Turbopack enabled (via running next dev --turbo), you may encounter Failed to compile Module not found compilation error referencing ./node_modules/keyv/src/index.js file or see Critical dependency: the request of a dependency is an expression warnings for the same keyv module.

To fix this, please add ably to the serverComponentsExternalPackages list in next.config.js (read more about this option here):

javascript
1const nextConfig = {
2 // ...
3 experimental: {
4 serverComponentsExternalPackages: ['ably'],
5 },
6};

The issue is coming from the fact that when using App Router specifically dependencies used inside Server Components and Route Handlers will automatically be bundled by Next.js. This causes issues with some packages, usually the ones that have complex require statements, for example, requiring some packages dynamically during runtime. keyv is one of those packages as it uses require statement dynamically when requiring its adapters (see code in repo):

keyv ends up being one of ably-js's upstream dependencies for node.js bundle, which causes the errors above when using it with Next.js App Router.

Using serverComponentsExternalPackages opt-outs from using Next.js bundling for specific packages and uses native Node.js require instead. This is a common problem in App Router for a number of packages (for example, see next.js issue vercel/next.js#52876), and using serverComponentsExternalPackages is the recommended approach here.

"Connection limit exceeded" error during development

If you're encountering a "Connection limit exceeded" error when trying to connect to Ably servers during the development of your application, and you notice spikes or linear increases in the connection count on the Ably dashboard for your app, this may be due to one of the following reasons:

  • If you're using Next.js, your Ably.Realtime client instance may be created multiple times on the server side (i.e., in a Node.js process) as you're developing your app, due to Next.js server side rendering your components. Note that even for "Client Components" (i.e., components with the 'use client' directive), Next.js may still run the component code on the server in order to pre-render HTML. Depending on your client configuration options, those clients may also successfully open a connection to Ably servers from that Node.js process, which won't close until you restart your development server.

    The simplest fix is to use the autoConnect client option and check if the client is created on the server side with a simple window object check, like this:

    typescript
    1const client = new Ably.Realtime({ key: 'your-ably-api-key', autoConnect: typeof window !== 'undefined' });

    This will prevent the client from connecting to Ably servers if it is created on the server side, while not affecting your client side components.

  • If you're using any React-based framework, you may be recreating the Ably.Realtime client instance on every component re-render. To avoid this, and to prevent potentially reaching the maximum connections limit on your account, move the client instantiation (new Ably.Realtime) outside of your components. You can find an example in our React docs.

  • The connection limit error can be caused by the Hot Reloading mechanism of your development environment (called Fast Refresh in newer Next.js versions, or more generally, Hot Module Replacement - HMR). When you edit and save a file that contains a new Ably.Realtime() call in an environment that supports HMR (such as React, Vite, or Next.js apps), the file gets refreshed and creates a new Ably.Realtime client instance. However, the previous client remains in memory, unaware of the replacement, and stays connected to Ably's realtime systems. As a result, your connection count will keep increasing with each file edit as new clients are created. This only resets when you manually refresh the browser page, which closes all clients. This behavior applies to any development environment with an HMR mechanism implemented.

    The solution is simple: move the new Ably.Realtime() call to a separate file, such as ably-client.js, and export the client instance from there. This way, the client instance will only be recreated when you specifically make changes to the ably-client.js file, which should be far less frequent than changes in the rest of the codebase.

Contributing

For guidance on how to contribute to this project, see the CONTRIBUTING.md.

Credits

Automated browser testing supported by

Repository image