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:
Authenticate
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
subscribe
for the channels you intend to subscribe to.history
if you intend to sync from historical messages.
Install
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
CopyCopied!
Import the SDKs into your project:
import ModelsClient from '@ably-labs/models';
import { Realtime } from 'ably/promises';
CopyCopied!
Instantiate a realtime client using the Ably JavaScript SDK and pass the generated client into the Models constructor:
const ably = new Realtime.Promise({ key: '...' });
const modelsClient = new ModelsClient({ ably });
CopyCopied!
ClientOptions
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
, ortrace
. 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:
const modelsClient = new ModelsClient({
client,
logLevel,
syncOptions: {
historyPageSize,
messageRetentionPeriod,
retryStrategy,
},
eventBufferOptions: {
bufferMs,
eventOrderer,
},
optimisticEventOptions: {
timeout,
},
});
CopyCopied!
Quickstart
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.
const model = modelsClient.models.get({
channelName: 'post:123',
sync,
merge,
});
CopyCopied!
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 sync function
Create a simple sync function that loads the post and its comments. The response should contain both:
- The data used to initialize the model.
- The maximum SequenceId from the outbox table.
async function sync(id: number, page: number) {
const result = await fetch(`/api/post/${id}?page=${page}`);
return result.json();
}
CopyCopied!
Below is an example result from the sync function:
{
"sequenceId": "1",
"data": {
"id": 123,
"text": "Hello World",
"comments": []
}
}
CopyCopied!
The Models SDK will infer the type of the model state from the type of the data payload returned by the sync function:
type Post = {
id: number;
text: string;
comments: string[];
};
CopyCopied!
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 merge function
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:
async function merge(state: Post, event: OptimisticEvent | ConfirmedEvent) {
if (event.name === 'addComment') {
return {
...state,
comments: state.comments.concat([event.data]),
};
}
// handle other event types
}
CopyCopied!
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:
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();
}
CopyCopied!
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;
CopyCopied!
Optimistic updates
Use optimistic updates to instantly update the model with the new comment data without waiting for confirmation from the backend:
// 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();
}
CopyCopied!