Setup

The Ably Models SDK is a standalone SDK built on Ably’s JavaScript SDK with full TypeScript support. It enables you to easily create live, observable data models in your frontend applications to ensure they remain synchronized with your database’s state in realtime.

The following diagram provides a simplified overview of the Models SDK:

Models SDK optimistic update

An API key is required to authenticate with Ably. API keys are used either to authenticate directly with Ably using basic authentication, or to generate tokens for untrusted clients using token authentication.

Sign up to Ably to create an API key in the dashboard or use the Control API to create an API programmatically.

API keys and tokens have a set of capabilities assigned to them that specify which operations, such as subscribe or publish can be performed on which resources. To use the Models SDK, the API key requires the following capabilities

The Models SDK requires a realtime client created using the Ably JavaScript SDK to interact with the Ably service.

Install the Ably JavaScript SDK and the Models SDK from NPM:

npm install ably @ably-labs/models
Copied!

Import the SDKs into your project:

Select...
import ModelsClient from '@ably-labs/models'; import { Realtime } from 'ably/promises';
Copied!

Instantiate a realtime client using the Ably JavaScript SDK and pass the generated client into the Models constructor:

Select...
const ably = new Realtime.Promise({ key: '...' }); const modelsClient = new ModelsClient({ ably });
Copied!

In addition to the underlying Ably realtime client, you can provide a number of other ClientOptions to configure the behavior of the Models SDK:

syncOptions
is used to configure how the model state is synchronised via the sync function.
historyPageSize
is the limit used when querying for paginated history used to subscribe to changes from the correct point in the channel.
messageRetentionPeriod
is the message retention period configured on the channel. This is used to determine whether the model state can be brought up to date from message history rather than via a re-sync.
retryStrategy
defines a retry strategy to use if calling the sync function throws an error.
eventBufferOptions
used to configure the in-memory sliding-window buffer used for reordering and deduplication.
optimisticEventOptions
is used to configure how optimistic events are applied.
logLevel
configures the log level used to control the verbosity of log output. One of fatal, error, warn, info, debug, or trace.
ModelsClient
captures a collection of named model instances used in your application and provides methods for creating new models.

The following is an example of setting ClientOptions when instantiating the Models SDK:

Select...
const modelsClient = new ModelsClient({ client, logLevel, syncOptions: { historyPageSize, messageRetentionPeriod, retryStrategy, }, eventBufferOptions: { bufferMs, eventOrderer, }, optimisticEventOptions: { timeout, }, });
Copied!

A model is a single instance of a live, observable data model backed by your database. In this guide, we will create a simple model that tracks a list of comments on a post.

To create the model, use the models.get() method on the client. If a model with the given name already exists, it will be returned.

To instantiate a Model you must provide a unique name. This identifies the model on the client, and is also the name of the channel used to subscribe to state updates from the backend.

Select...
const model = modelsClient.models.get({ channelName: 'post:123', sync, merge, });
Copied!

The model also requires:

  • Sync function to initialize the model’s state from the backend.
  • Merge function to calculate the next version of the model state when change events are received from the backend.

Create a simple sync function that loads the post and its comments. The response should contain both:

  1. The data used to initialize the model.
  2. The maximum SequenceId from the outbox table.
Select...
async function sync(id: number, page: number) { const result = await fetch(`/api/post/${id}?page=${page}`); return result.json(); }
Copied!

Below is an example result from the sync function:

{ "sequenceId": "1", "data": { "id": 123, "text": "Hello World", "comments": [] } }
Copied!

The Models SDK will infer the type of the model state from the type of the data payload returned by the sync function:

Select...
type Post = { id: number; text: string; comments: string[]; };
Copied!

The sync endpoint on the backend returns the post data as well as a sequenceId which defines the point in the stream of change events that corresponds to this version of the data. You can obtain the sequenceId by reading the largest sequenceId from the outbox table in the same transaction that queries the post data.

Create a simple merge function which defines how to calculate the next version of the model state when a change event is received from the backend. In this case, you will append the new comment to the list when an addComment event is received:

Select...
async function merge(state: Post, event: OptimisticEvent | ConfirmedEvent) { if (event.name === 'addComment') { return { ...state, comments: state.comments.concat([event.data]), }; } // handle other event types }
Copied!

Whenever new comments are added to the post, the model will be updated in realtime.

The following function will add a new comment using a backend endpoint:

Select...
async function updatePost(id: number, mutationId: string, comment: string) { const result = await fetch(`/api/post/${id}/comments`, { method: 'POST', body: JSON.stringify({ mutationId, comment }), }); return result.json(); }
Copied!

On the backend, the endpoint inserts the new comment in the database and transactionally writes an addComment change event with the provided mutationId to the outbox table. This change event record is then broadcast to other clients subscribed to this model via the Database Connector. The following example demonstrates this:

BEGIN; -- mutate your data, e.g.: INSERT INTO comments (comment) VALUES ('New comment!'); -- write change event to outbox, e.g.: INSERT INTO outbox (mutation_id, channel, name, data) VALUES ('my-mutation-id', 'posts:123', 'addComment', 'New comment!'); COMMIT;
Copied!

Use optimistic updates to instantly update the model with the new comment data without waiting for confirmation from the backend:

Select...
// optimistically apply the changes to the model const [confirmation, cancel] = await model.optimistic({ mutationId: 'my-mutation-id', name: 'addComment', data: 'New comment!', }); try { // apply the changes in your backend await updatePost('my-mutation-id', 'New comment!'); // wait for the optimistic event to be confirmed await confirmation; } catch (err) { // something went wrong, cancel the optimistic update cancel(); }
Copied!
Authenticate
v2.0