Objects

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.

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 Object

LiveMap is a key/value data structure similar to a dictionary or JavaScript Map:

  • Keys must be strings
  • Values can be primitive types or references to other objects
  • Supports set and remove operations
  • Concurrent updates to the same key are resolved using last-write-wins (LWW) semantics
JavaScript

1

2

3

4

5

6

// Create a LiveMap
const userSettings = await channel.objects.createMap();

// Set primitive values
await userSettings.set('theme', 'dark');
await userSettings.set('notifications', true);

Primitive Types

LiveMap supports the following primitive types as values:

  • string
  • number
  • boolean
  • bytes

LiveCounter Object

LiveCounter is a numeric counter type:

  • The value is a double-precision floating-point number
  • Supports increment and decrement operations
JavaScript

1

2

3

4

5

// Create a LiveCounter
const visitsCounter = await channel.objects.createCounter();

// Increment the counter
await visitsCounter.increment(1);

Root Object

The root 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 root object using the getRoot() function:

JavaScript

1

2

3

4

5

// Get the Root Object
const root = await channel.objects.getRoot();

// Use it like any other LiveMap
await root.set('app-version', '1.0.0');

Reachability

All objects must be reachable from the root object (directly or indirectly). Objects that cannot be reached from the root object will eventually be deleted.

When an object has been deleted, it is no longer usable and calling any methods on the object will fail.

In the example below, the only reference to the counterOld object is replaced on the root. This makes counterOld unreachable and it will eventually be deleted.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

// Create a counter and reference it from the root
const counterOld = await channel.objects.createCounter();
await root.set('myCounter', counterOld);

// counterOld will eventually be deleted
counterOld.on('deleted', () => {
  console.log('counterOld has been deleted and can no longer be used');
});

// Create a new counter and replace the old one referenced from the root
const counterNew = await channel.objects.createCounter();
await root.set('myCounter', counterNew);

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.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

// Create LiveObjects
const profileMap = await channel.objects.createMap();
const preferencesMap = await channel.objects.createMap();
const activityCounter = await channel.objects.createCounter();

// Build a composite structure
await preferencesMap.set('theme', 'dark');
await profileMap.set('preferences', preferencesMap);
await profileMap.set('activity', activityCounter);
await root.set('profile', profileMap);

// Resulting structure:
// root (LiveMap)
// └── profile (LiveMap)
//     ├── preferences (LiveMap)
//     │   └── theme: "dark" (string)
//     └── activity (LiveCounter)

It is possible for the same object instance to be accessed from multiple places in your object tree:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

// Create a counter
const counter = await channel.objects.createCounter();

// Create two different maps
const mapA = await channel.objects.createMap();
const mapB = await channel.objects.createMap();
await root.set('a', mapA);
await root.set('b', mapB);

// Reference the same counter from both maps
await mapA.set('count', counter);
await mapB.set('count', counter);

// The counter referenced from each location shows the same
// value, since they refer to the same underlying counter
mapA.get('count').subscribe(() => {
  console.log(mapA.get('count').value()); // 1
});
mapB.get('count').subscribe(() => {
  console.log(mapB.get('count').value()); // 1
});

// Increment the counter
await counter.increment(1);

It is also possible that object references form a cycle:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// Create two different maps
const mapA = await channel.objects.createMap();
const mapB = await channel.objects.createMap();

// Set up a circular reference
await mapA.set('ref', mapB);
await mapB.set('ref', mapA);

// Add one map to root (both are now reachable)
await root.set('a', mapA);

// We can traverse the cycle
root.get('a') // mapA
  .get('ref')  // mapB
  .get('ref'); // mapA

Metadata

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.

Object IDs follow a specific format:

1

type:hash@timestamp

For example:

1

counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000

This format has been specifically designed to ensure uniqueness in a globally distributed system and includes:

PartDescription
typeThe object type (either map or counter).
hashA base64 string encoded hash derived from the initial value of the object and a random nonce.
timestampA Unix millisecond timestamp denoting the creation time of the object.

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

Select...