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.

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 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 v2.9
// Create a LiveMap const userSettings = await channel.objects.createMap(); // Set primitive values await userSettings.set('theme', 'dark'); await userSettings.set('notifications', true);
Copied!

LiveMap supports the following primitive types as values:

  • string
  • number
  • boolean
  • bytes

LiveCounter is a numeric counter type:

  • The value is a double-precision floating-point number
  • Supports increment and decrement operations
JavaScript v2.9
// Create a LiveCounter const visitsCounter = await channel.objects.createCounter(); // Increment the counter await visitsCounter.increment(1);
Copied!

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 v2.9
// Get the Root Object const root = await channel.objects.getRoot(); // Use it like any other LiveMap await root.set('app-version', '1.0.0');
Copied!

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 v2.9
// 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);
Copied!

LiveObjects enables you to build complex, hierarchical data structures through composability.

Specifically, a LiveMap:/docs/liveobjects/map can store references to other LiveMap or LiveCounter object instances as values. This allows you to create nested hierarchies of data.

JavaScript v2.9
// 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)
Copied!

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

JavaScript v2.9
// 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);
Copied!

It is also possible that object references form a cycle:

JavaScript v2.9
// 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
Copied!

Objects include metadata that helps with synchronization, conflict resolution and managing the object lifecycle.

Every object has a unique identifier that distinguishes it from all other objects.

Object IDs follow a specific format:

type:hash@timestamp
Copied!

For example:

counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000
Copied!

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

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

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.

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