Instance

Open in

Instance provides a reference to a specific LiveObject instance (such as a LiveMap or LiveCounter) that you can manipulate directly. Unlike PathObject, which operates on paths that resolve dynamically at runtime, an Instance is bound to a specific object instance identified by its object ID.

An Instance represents a specific object instance within the channel object. When you call methods on an Instance, they operate on that specific instance regardless of where it exists in the structure or whether it has been moved.

Get an Instance

Obtain an Instance from a PathObject using the instance() method:

JavaScript

1

2

3

4

5

6

7

// Get a PathObject for the channel object
const myObject = await channel.object.get();

// Get the specific Instance of a LiveCounter located at the 'visits' key
const visits = myObject.get('visits').instance();
console.log(counterInstance?.id); // e.g. counter:abc123@1234567890
console.log(counterInstance?.value()); // e.g. 5

The instance() method returns undefined if no object exists at that path.

Once you have an Instance, it references a specific object by its ID. This means operations target that exact object:

JavaScript

1

2

3

4

5

// Obtain an Instance for a LiveCounter
const visits = myObject.get('visits').instance();

// Increment the specific LiveCounter instance
await visits?.increment(5);

An Instance references a specific object by its ID rather than by location.

Type inference

When using TypeScript, you can provide type parameters to instance() to get rich type inference:

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

25

26

import { type Instance, LiveCounter, LiveMap } from 'ably/liveobjects';

type Settings = {
  theme: string;
  notifications: boolean;
};

type MyObject = {
  visits: LiveCounter;
  settings: LiveMap<Settings>;
};

const myObject = await channel.object.get<MyObject>();

// TypeScript knows 'visits' is a LiveCounter
const visits: Instance<LiveCounter> | undefined = myObject.get('visits').instance();
if (visits) {
  await visits.increment(1); // Type-safe
}

// TypeScript knows the shape of 'settings'
const settings: Instance<LiveMap<Settings>> | undefined = myObject.get('settings').instance();
if (settings) {
  const theme = settings.get('theme');
  console.log(theme?.value()); // Returns string | undefined
}

See the Typing documentation for more details on type safety.

For LiveMap instances, use the get(key) method to navigate to a child value:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

const myObject = await channel.object.get();

// Obtain an instance
const settings = myObject.get('settings').instance();
console.log(settings?.id); // e.g. "map:abc123@1234567890"

// Chain get() calls for deeper navigation
// get() may return undefined at any point if the instance
// is not a LiveMap or the accessed entry does not exist
const color = settings?.get('theme')?.get('color');
console.log(color?.value()); // e.g. blue

The id getter returns the object ID of the instance, which can be used with the REST API.

Access data

Use the get(key) method to access a value from a LiveMap instance. The returned Instance represents whatever exists at that entry. What you can do with that Instance depends on whether the entry contains a primitive value, a LiveCounter, or a nested LiveMap.

Read values

When an entry contains a primitive value or LiveCounter stored directly in the LiveMap, use the value() method to get the current value:

JavaScript

1

2

3

4

5

6

7

8

9

10

// Get an instance
const user = myObject.get('user').instance();

// Get the value of an entry containing a primitive
const username = user?.get('username');
console.log('Username:', username?.value()); // e.g. "alice"

// Get the value of the LiveCounter stored in 'visits'
const visits = user?.get('visits');
console.log(visits?.value()); // e.g. 5

If the entry doesn't exist, get() returns undefined:

JavaScript

1

2

3

4

5

const user = myObject.get('user').instance();
if (user) {
  // Get an entry that doesn't exist on the instance
  console.log(user.get('nonexistent')); // undefined
}

If the entry exists, but does not contain a primitive or a LiveCounter instance, value() returns undefined:

JavaScript

1

2

3

4

5

6

7

8

const myObject = await channel.object.get();

// Set an entry that contains a nested LiveMap
await myObject.set('settings', LiveMap.create({ theme: 'dark' }));

// Calling value() on a LiveMap returns undefined
const settings = myObject.get('settings').instance();
console.log(settings?.value()); // undefined - it's a LiveMap, not a primitive or LiveCounter

Enumerate collections

For LiveMap instances, you can iterate over the entries, keys, and values:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

const settings = myObject.get('settings').instance();

if (settings) {
  // Iterate over key-value pairs.
  // Each key is a string, and the value is an Instance for the entry.
  for (const [key, value] of settings.entries()) {
    console.log(`${key}:`, value.value());
  }

  // Iterate over keys only
  for (const key of settings.keys()) {
    console.log('Key:', key);
  }

  // Iterate over values
  for (const value of settings.values()) {
    console.log('Value:', value.value());
  }
}

Collection methods return empty iterators if the instance is not a LiveMap:

JavaScript

1

2

3

4

5

6

7

const visits = myObject.get('visits').instance(); // This is a LiveCounter, not a LiveMap

if (visits) {
  for (const [key, value] of visits.entries()) {
    // This loop doesn't execute - entries() returns empty iterator
  }
}

Get the size of a collection

Use the size() method to get the number of entries in a LiveMap:

JavaScript

1

2

3

4

const settings = myObject.get('settings').instance();

// Get the number of entries
console.log(settings?.size());

The size() method returns undefined if the instance is not a LiveMap:

JavaScript

1

2

const visits = myObject.get('visits').instance(); // This is a LiveCounter, not a LiveMap
console.log(visits?.size()); // undefined

Get a compact object

The compact() method returns a JavaScript object representation of the instance:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

const settingsInstance = myObject.get('settings').instance();

if (settingsInstance) {
  const compact = settingsInstance.compact();
  console.log(compact);
  // {
  //   theme: { color: 'dark', fontSize: 14 },
  //   notifications: true
  // }
}

const counterInstance = myObject.get('visits').instance();
if (counterInstance) {
  console.log(counterInstance.compact()); // Just the number, e.g., 42
}

Binary data in the channel object (such as ArrayBuffer/Uint8Array in browser environments, or Buffer in Node.js) is preserved as typed arrays in the returned object.

If the instance contains cyclic references, these are preserved in the returned data structure:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

// Imagine a LiveMap instance has this structure:
// user (LiveMap)
//   ├─ profile -> references user (creates cycle)
//   └─ name: "Alice"

const userInstance = myObject.get('user').instance();
if (userInstance) {
  // When you call compact(), cyclic references are preserved as actual JS references
  const compacted = userInstance.compact();
  console.log(compacted.profile === compacted); // true - actual cyclic reference

  // You can navigate the cycle
  console.log(compacted.profile.profile.name); // "Alice"

  // This will throw an error because cycles can't be serialized
  JSON.stringify(compacted); // ❌ TypeError: cyclic object value
}

It is possible for the value returned from compact() to contain cyclic references, so it is not safe to serialize this value with JSON.stringify().

Use the compactJson() method to obtain a value that can be safely passed to JSON.stringify():

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// Example: Using the same cyclic structure from before
// user (LiveMap)
//   ├─ profile -> references user (creates cycle)
//   └─ name: "Alice"

const userInstance = myObject.get('user').instance();
if (userInstance) {
  // compactJson() breaks cycles, making it safe to serialize
  const compactJson = userInstance.compactJson();
  console.log(compactJson);

  // This can be safely serialized as the
  // cycle is broken via an objectId reference
  console.log(JSON.stringify(compactJson));
  // {
  //   "profile": { "objectId": "map:abc123@1234567890" },
  //   "name": "Alice"
  // }
}

Binary data in the channel object are serialized as base64-encoded strings by compactJson():

JavaScript

1

2

3

4

5

6

7

8

9

10

11

const mapInstance = myObject.instance();

if (mapInstance) {
  // Store binary data in the LiveMap
  const binaryData = new TextEncoder().encode("world");
  await mapInstance.set('hello', binaryData);

  // compactJson() converts binary data to base64 strings
  const compactJson = mapInstance.compactJson();
  console.log(compactJson.hello); // "d29ybGQ=" (base64 encoded)
}

Update data

Instance provides mutation methods that operate on the specific instance. The specific methods available depend on the type of the instance:

  • For LiveMap instances, you can use methods like set() and remove(). See the LiveMap documentation for details on these methods.
  • For LiveCounter instances, you can use methods like increment() and decrement(). See the LiveCounter documentation for details on these methods.

When you call a method on an Instance, it directly updates that specific instance.

JavaScript

1

2

3

4

5

6

7

8

9

// Update a LiveMap instance
const settings = myObject.get('settings').instance();
await settings?.set('theme', 'dark');
await settings?.remove('oldSetting');

// Update a LiveCounter instance
const visits = myObject.get('visits').instance();
await visits?.increment(5);
await visits?.decrement(2);

Batch multiple updates

The batch(callback) method groups multiple mutations into a single message. The ctx parameter is the batch context for the specific instance:

JavaScript

1

2

3

4

5

6

const settings = myObject.get('settings').instance();
await settings?.batch((ctx) => {
  ctx.set('theme', 'dark');
  ctx.get('preferences')?.set('language', 'en');
  ctx.get('volume')?.increment(5);
});

See the Batch operations documentation for more details on batching.

Subscribe to changes

Use the subscribe() method to be notified when the instance is updated:

JavaScript

1

2

3

4

5

6

7

8

9

10

const visits = myObject.get('visits').instance();

if (visits) {
  const { unsubscribe } = visits.subscribe(() => {
    console.log('Visits updated');
  });

  // Later, stop listening to changes
  unsubscribe();
}

Alternatively, use the subscribeIterator() method for an async iterator syntax:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

const visits = myObject.get('visits').instance();

if (visits) {
  for await (const _ of visits.subscribeIterator()) {
    console.log('Visits updated');

    if (someCondition) {
      break; // Unsubscribes
    }
  }
}

Instance subscriptions observe a specific object instance rather than a location. If the instance is moved to a different location within the channel object, the subscription continues to observe the same instance.

When an object instance is deleted after becoming unreachable, subscriptions to that instance are automatically unsubscribed after one final notification:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

const visits = myObject.get('visits').instance();

if (visits) {
  visits.subscribe(({ object, message }) => {
    if (message?.operation.action === 'object.delete') {
      console.log('visits counter was deleted');
      // This listener is automatically unsubscribed after this call
    }
  });

  // Make the LiveCounter stored in visits unreachable.
  // It will eventually be deleted.
  await myObject.remove('visits');
}

Determine what changed

The subscription receives an argument with information about the update:

  • The object field contains an Instance representing the same object instance you called subscribe() on
  • The message field contains the ObjectMessage which details the operation that caused the change, including information about the client that performed the operation and the specific changes made.
JavaScript

1

2

3

4

5

6

7

8

9

10

const visits = myObject.get('visits').instance();

if (visits) {
  visits.subscribe(({ object, message }) => {
    console.log('New value:', object.value());
    console.log('Updated by:', message?.clientId);
    console.log('Operation:', message?.operation.action);
    console.log(object.id === visits.id); // true
  });
}

Updates to nested objects

Instance subscriptions only observe changes to the specific instance that is subscribed. If an update is made to a nested child object, that is not surfaced on an instance subscription:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

const settings = myObject.get('settings').instance();

if (settings) {
  // Subscribe to the settings LiveMap instance
  settings.subscribe(({ object }) => {
    console.log('Settings LiveMap updated');
  });

  // This triggers the subscription (direct change to settings)
  await settings.set('theme', 'dark');

  // This does NOT trigger the subscription (change to nested object)
  await settings.get('preferences')?.set('language', 'en');
}