Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ably/docs/llms.txt
Use this file to discover all available pages before exploring further.
LiveObjects enables you to store shared data as “objects” on a channel, allowing your application data to be synchronized across multiple users and devices in realtime. This document explains the key concepts you need to know when working with objects.
Primitive types
Primitive types are the fundamental data types that can be stored in a collection type. Currently, the only supported collection type is a LiveMap.
LiveObjects supports the following primitive types:
string
number (double-precision floating-point)
boolean
bytes
- JSON arrays or objects
Object types
LiveObjects provides specialized object types to model your application state. These object types are designed to be conflict-free and eventually consistent, meaning that all operations on them are commutative and converge to the same state across all clients.
LiveMap
LiveMap is a key/value data structure similar to a dictionary or JavaScript Map:
- Keys must be strings
- Values can be primitive types, JSON-serializable objects or arrays, or references to other objects
- Supports
set and remove operations
- Concurrent updates to the same key are resolved using last-write-wins (LWW) semantics
// Create and assign a LiveMap
await myObject.set('settings', LiveMap.create({
theme: 'dark',
notifications: true
}));
// Access via path
const settings = myObject.get('settings');
await settings.set('fontSize', 14);
LiveCounter
LiveCounter is a numeric counter type:
- The value is a double-precision floating-point number
- Supports
increment and decrement operations
// Create and assign a LiveCounter
await myObject.set('visits', LiveCounter.create(0));
// Access via path
const visits = myObject.get('visits');
await visits.increment(1);
Channel object
The channel object is a special LiveMap instance which:
- Implicitly exists on a channel and does not need to be created explicitly
- Has the special objectId of
root
- Cannot be deleted
- Serves as the entry point for accessing all other objects on a channel
Access the channel object using channel.object.get(), which returns a PathObject that resolves to the root LiveMap:
// Get the channel object.
// This implicitly attaches to the channel if not already attached.
// The promise resolves when the channel object data has been synchronized to the client.
const myObject = await channel.object.get();
// Use it like any other LiveMap
await myObject.set('app-version', '1.0.0');
await myObject.set('config', LiveMap.create({
apiUrl: 'https://api.example.com'
}));
Composability
LiveObjects enables you to build complex, hierarchical data structures through composability.
Specifically, a LiveMap can store references to other LiveMap or LiveCounter object instances as values. This allows you to create nested hierarchies of data.
// Create a nested structure in a single operation
await myObject.set('profile', LiveMap.create({
name: 'Alice',
preferences: LiveMap.create({
theme: 'dark',
fontSize: 14
}),
activity: LiveCounter.create(0)
}));
// Resulting structure:
// myObject (LiveMap - channel object)
// └── profile (LiveMap)
// ├── name: "Alice" (string)
// ├── preferences (LiveMap)
// │ ├── theme: "dark" (string)
// │ └── fontSize: 14 (number)
// └── activity (LiveCounter)
// Access and update nested values via paths
await myObject.get('profile').get('activity').increment(5);
await myObject.get('profile').get('preferences').set('theme', 'light');
Reachability
All objects must be reachable from the channel object (directly or indirectly). Objects that cannot be reached from the channel object will eventually be deleted.
When you replace or remove an object reference, it may become unreachable and will eventually be deleted:
// Create and assign a counter
await myObject.set('visits', LiveCounter.create(0));
// Get the counter instance to track it
const oldCounter = myObject.get('visits').instance();
if (oldCounter) {
oldCounter.subscribe(({ object, message }) => {
if (message?.operation.action === 'object.delete') {
console.log('Counter was deleted');
}
});
// Replace with a new counter - old counter becomes unreachable
await myObject.set('visits', LiveCounter.create(0));
// The subscription will fire with the delete notification
}
Objects include metadata that helps with synchronization, conflict resolution and managing the object lifecycle.
Object IDs
Every object has a unique identifier that distinguishes it from all other objects.
You can access an object’s ID using the Instance API:
const counterInstance = myObject.get('visits').instance();
if (counterInstance) {
const objectId = counterInstance.id();
console.log('Object ID:', objectId);
// e.g., "counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000"
}
Object IDs are opaque strings that uniquely identify each object instance. The client library automatically manages object IDs and allows you to work with object references directly. However, you may specify object IDs explicitly when using the REST API.
Tombstones
Tombstones are markers indicating an object or map entry has been deleted.
- A tombstone is created for an object when it becomes unreachable from the channel object.
- A tombstone is created for a map entry when it is removed
Tombstones protect against lagging clients from re-introducing a deleted value, ensuring all clients eventually converge on the same state. They are eventually garbage collected after a safe period of time.
Timeserials
When an operation message is published it is assigned a unique logical timestamp called a “timeserial”.
This timeserial is stored on map entries in order to implement last-write-wins conflict resolution semantics.
Additionally, all objects store the timeserial of the last operation that was applied to the object. Since Ably operates fully independent data centers, these timeserials are stored on a per-site basis.
Timeserial metadata is used for internal purposes and is not directly exposed in client libraries. However, it can be viewed using the REST API.