# Instance
`Instance` provides a reference to a specific LiveObject instance (such as a `LiveMap` or `LiveCounter`) that you can manipulate directly. Unlike [PathObject](https://ably.com/docs/liveobjects/concepts/path-object.md), 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.
An `Instance` can also wrap a primitive value, for example when obtained from a path that resolves to a primitive. Primitive-backed instances are read-only and have no object ID.
## Get an Instance
Obtain an `Instance` from a `PathObject` using the `instance()` method:
### Javascript
```
// Get a PathObject for the channel object
const rootObject = await channel.object.get();
// Get the specific Instance of a LiveCounter located at the 'visits' key
const visits = rootObject.get('visits').instance(); // undefined only if nothing exists at the path
console.log(visits?.id); // e.g. counter:abc123@1234567890
console.log(visits?.value()); // e.g. 5
```
### Swift
```
// Get a PathObject for the channel object
let rootObject = try await channel.object.get()
// Get the specific Instance of a LiveCounter located at the 'visits' key
let visitsInstance = try rootObject.get(key: "visits").instance() // nil only if nothing exists at the path
if case .liveCounter(let visits)? = visitsInstance {
print(visits.id) // e.g. counter:abc123@1234567890
print(try visits.value) // e.g. 5.0
}
```
### Java
```
// Get a PathObject for the channel object
LiveMapPathObject rootObject = channel.object.get().join();
// Get the specific Instance of a LiveCounter located at the 'visits' key
Instance visitsInstance = rootObject.get("visits").instance(); // null only if nothing exists at the path
if (visitsInstance != null) {
LiveCounterInstance visits = visitsInstance.asLiveCounter(); // throws IllegalStateException on mismatch
System.out.println(visits.getId()); // e.g. "counter:abc123@1234567890"
System.out.println(visits.value()); // e.g. 5.0
}
```
The `instance()` method returns `undefined` only if nothing exists at that path. It wraps whatever value resolves there: a `LiveMap`, a `LiveCounter`, or a primitive value. A primitive-backed `Instance` is read-only: it has no `id` and exposes the primitive via `value()`.
The `instance()` method returns `nil` only if nothing exists at that path. It wraps whatever value resolves there, surfaced as the corresponding case of the `Instance` enum: a `LiveMap` becomes `.liveMap`, a `LiveCounter` becomes `.liveCounter`, and a primitive value becomes `.primitive`. A primitive-backed `Instance` (`.primitive`) is read-only: it has no `id` and exposes the value through its `value` property.
The `instance()` method returns `null` only if nothing exists at that path. It wraps whatever value resolves there: a `LiveMap` or `LiveCounter` becomes a `LiveMapInstance` or `LiveCounterInstance`, and a primitive value becomes a read-only primitive instance (for example `StringInstance`).
Once you have an `Instance`, it references a specific object by its ID. This means operations target that exact object:
### Javascript
```
// Obtain an Instance for a LiveCounter
const visits = rootObject.get('visits').instance();
// Increment the specific LiveCounter instance
await visits?.increment(5);
```
### Swift
```
// Obtain an Instance for a LiveCounter
let visitsInstance = try rootObject.get(key: "visits").instance()
// Increment the specific LiveCounter instance
if case .liveCounter(let visits)? = visitsInstance {
try await visits.increment(amount: 5)
}
```
### Java
```
// Obtain an Instance for a LiveCounter
Instance visitsInstance = rootObject.get("visits").instance();
// Increment the specific LiveCounter instance
if (visitsInstance != null) {
visitsInstance.asLiveCounter().increment(5).join();
}
```
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
```
import { type Instance, LiveCounter, LiveMap } from 'ably/liveobjects';
type Settings = {
theme: string;
notifications: boolean;
};
type RootObject = {
visits: LiveCounter;
settings: LiveMap;
};
const rootObject = await channel.object.get();
// TypeScript knows 'visits' is a LiveCounter
const visits: Instance | undefined = rootObject.get('visits').instance();
if (visits) {
await visits.increment(1); // Type-safe
}
// TypeScript knows the shape of 'settings'
const settings: Instance> | undefined = rootObject.get('settings').instance();
if (settings) {
const theme = settings.get('theme');
console.log(theme?.value()); // Returns string | undefined
}
```
See the [Typing documentation](https://ably.com/docs/liveobjects/typing.md) for more details on type safety.
## Type inference
An `Instance` wraps a value whose type is already known, so its `as*` casts **throw** an `IllegalStateException` when the type doesn't match. This is the opposite of the `PathObject` casts, which never throw. In exchange, reads on a typed instance never return `null`. Check `getType()` first when the type isn't known; `getType()` on an `Instance` is non-null.
### Java
```
Instance visitsInstance = rootObject.get("visits").instance();
if (visitsInstance != null && visitsInstance.getType() == ValueType.LIVE_COUNTER) {
visitsInstance.asLiveCounter().increment(1).join();
}
```
See the [Type inference documentation](https://ably.com/docs/liveobjects/typing.md#type-inference) for the full contract, including how the path-layer casts differ.
## Type inference
`Instance` is an enum with three cases: `.liveMap`, `.liveCounter` and `.primitive`. There are no casts on an `Instance`; discriminate between the cases with an exhaustive `switch`, which the compiler checks. A type mismatch error (code 92007, which the path layer throws for wrong-typed writes) cannot occur from this discrimination. In exchange, reads on a typed instance never return `nil` for a type mismatch: `try map.size` and `try counter.value` are non-optional.
### Swift
```
switch try rootObject.get(key: "visits").instance() {
case .liveCounter(let visits):
try await visits.increment(amount: 1)
case .liveMap(let map):
print(try map.size)
case .primitive(let primitive):
print(try primitive.value)
case .none:
print("nothing exists at this path")
}
```
A `PrimitiveInstance` wraps a `Primitive`, which you `switch` over in the same way. When you already know which primitive type to expect, read it through the matching per-case property instead: `stringValue`, `numberValue`, `boolValue`, `dataValue`, `jsonArrayValue` or `jsonObjectValue`. Each returns `nil` when the wrapped primitive is of a different case:
### Swift
```
if case .primitive(let theme)? = try rootObject.at(path: "settings.theme").instance() {
print(try theme.stringValue ?? "") // e.g. "dark"
}
```
An instance is already bound to a resolved value, so these are properties rather than the methods of the same name on a [`PathObject`](https://ably.com/docs/liveobjects/concepts/path-object.md#typing).
Use `if case` when you only need to handle one type:
### Swift
```
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
try await visits.increment(amount: 1)
}
```
When you only need to inspect the type, read the `type` property on the `Instance`, which is non-optional and runs in constant time:
### Swift
```
if let instance = try rootObject.get(key: "visits").instance() {
print(instance.type) // e.g. liveCounter
}
```
Reads on an instance throw an `ARTErrorInfo` only when LiveObjects cannot be accessed at all: the channel is in the `DETACHED` or `FAILED` state (error code 90001), or the channel is missing the `object_subscribe` mode (error code 40024). They never throw for a type mismatch or an absent entry.
See the [Type inference documentation](https://ably.com/docs/liveobjects/typing.md#type-inference) for the full contract, including how the path-layer casts differ.
## Navigate an Instance
For `LiveMap` instances, use the `get(key)` method to navigate to a child value:
### Javascript
```
const rootObject = await channel.object.get();
// Obtain an instance
const settings = rootObject.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
```
### Swift
```
// Obtain an instance
if case .liveMap(let settings)? = try rootObject.get(key: "settings").instance() {
print(settings.id) // e.g. "map:abc123@1234567890"
// Chain get() calls for deeper navigation.
// get(key:) returns nil at any point if the accessed entry does not exist
if case .liveMap(let theme)? = try settings.get(key: "theme"),
case .primitive(let color)? = try theme.get(key: "color"),
case .string(let colorValue) = try color.value {
print(colorValue) // e.g. "blue"
}
}
```
### Java
```
// Obtain an instance
Instance settingsInstance = rootObject.get("settings").instance();
if (settingsInstance != null) {
LiveMapInstance settings = settingsInstance.asLiveMap();
System.out.println(settings.getId()); // e.g. "map:abc123@1234567890"
// Chain get() calls for deeper navigation
// get() may return null at any point if the accessed entry does not exist
Instance theme = settings.get("theme");
if (theme != null && theme.getType() == ValueType.LIVE_MAP) {
Instance color = theme.asLiveMap().get("color");
if (color != null) {
System.out.println(color.asString().value()); // e.g. "blue"
}
}
}
```
The `id` getter returns the [object ID](https://ably.com/docs/liveobjects/concepts/objects.md#object-ids) of the instance, which can be used with the [REST API](https://ably.com/docs/liveobjects/rest-api-usage.md). On a primitive-backed instance, `id` returns `undefined`, because primitives have no object ID.
The `id` property returns the [object ID](https://ably.com/docs/liveobjects/concepts/objects.md#object-ids) of the instance, which can be used with the [REST API](https://ably.com/docs/liveobjects/rest-api-usage.md). A primitive-backed instance has no `id` property, because primitives have no object ID.
The `getId()` method returns the [object ID](https://ably.com/docs/liveobjects/concepts/objects.md#object-ids) of the instance, which can be used with the [REST API](https://ably.com/docs/liveobjects/rest-api-usage.md).
## 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
```
// Get an instance
const user = rootObject.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
```
#### Swift
```
// Get an instance
if case .liveMap(let user)? = try rootObject.get(key: "user").instance() {
// Get the value of an entry containing a primitive
if case .primitive(let username)? = try user.get(key: "username"),
case .string(let name) = try username.value {
print("Username: \(name)") // e.g. "alice"
}
// Get the value of the LiveCounter stored in 'visits'
if case .liveCounter(let visits)? = try user.get(key: "visits") {
print(try visits.value) // e.g. 5.0
}
}
```
#### Java
```
// Get an instance
Instance userInstance = rootObject.get("user").instance();
if (userInstance != null) {
LiveMapInstance user = userInstance.asLiveMap();
// Get the value of an entry containing a primitive
Instance username = user.get("username");
if (username != null) {
System.out.println("Username: " + username.asString().value()); // e.g. "alice"
}
// Get the value of the LiveCounter stored in 'visits'
Instance visits = user.get("visits");
if (visits != null) {
System.out.println(visits.asLiveCounter().value()); // e.g. 5.0
}
}
```
If the entry doesn't exist, `get()` returns undefined:
If the entry doesn't exist, `get(key:)` returns `nil`:
If the entry doesn't exist, `get()` returns `null`:
#### Javascript
```
const user = rootObject.get('user').instance();
if (user) {
// Get an entry that doesn't exist on the instance
console.log(user.get('nonexistent')); // undefined
}
```
#### Swift
```
if case .liveMap(let user)? = try rootObject.get(key: "user").instance() {
// Get an entry that doesn't exist on the instance
print(String(describing: try user.get(key: "nonexistent"))) // nil
}
```
#### Java
```
Instance userInstance = rootObject.get("user").instance();
if (userInstance != null) {
// Get an entry that doesn't exist on the instance
System.out.println(userInstance.asLiveMap().get("nonexistent")); // null
}
```
If the entry exists, but does not contain a primitive or a `LiveCounter` instance, `value()` returns `undefined`:
#### Javascript
```
const rootObject = await channel.object.get();
// Set an entry that contains a nested LiveMap
await rootObject.set('settings', LiveMap.create({ theme: 'dark' }));
// Calling value() on a LiveMap returns undefined
const settings = rootObject.get('settings').instance();
console.log(settings?.value()); // undefined - it's a LiveMap, not a primitive or LiveCounter
```
In Java, a wrong-typed call cannot happen: the method simply doesn't exist on the typed instance, and casting to the wrong type throws an `IllegalStateException`. Check [`getType()`](https://ably.com/docs/liveobjects/typing.md#get-type) when the type isn't known. See the [Type inference documentation](https://ably.com/docs/liveobjects/typing.md#type-inference) for the full cast contract:
#### Java
```
Instance settingsInstance = rootObject.get("settings").instance(); // a LiveMap
if (settingsInstance != null) {
System.out.println(settingsInstance.getType()); // LIVE_MAP
// Maps have no value() and counters have no entries()/size() - those methods
// simply don't exist on the wrong type. Casting to the wrong type throws:
settingsInstance.asLiveCounter(); // throws IllegalStateException
}
```
In Swift, a wrong-typed call cannot happen: the method simply doesn't exist on the typed instance, and the `Instance` enum has no casts to get wrong. Match the case you need, or check the [`type`](https://ably.com/docs/liveobjects/typing.md#get-type) property when the type isn't known. See the [Type inference documentation](https://ably.com/docs/liveobjects/typing.md#type-inference) for the full contract:
#### Swift
```
if let settings = try rootObject.get(key: "settings").instance() { // a LiveMap
print(settings.type) // liveMap
// Maps expose entries()/keys()/values()/size and counters expose value -
// those members simply don't exist on the wrong type, so the compiler
// rejects a wrong-typed call before it can run:
if case .liveMap(let map) = settings {
print(try map.size)
}
}
```
### Enumerate collections
For `LiveMap` instances, you can iterate over the entries, keys, and values:
#### Javascript
```
const settings = rootObject.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());
}
}
```
#### Swift
```
if case .liveMap(let settings)? = try rootObject.get(key: "settings").instance() {
// Iterate over key-value pairs.
// Each key is a String, and the value is an Instance for the entry.
for (key, value) in try settings.entries() {
print("\(key): \(try value.compactJson())")
}
// Iterate over keys only
for key in try settings.keys() { print("Key: \(key)") }
// Iterate over values
for value in try settings.values() { print("Value: \(try value.compactJson())") }
}
```
#### Java
```
Instance settingsInstance = rootObject.get("settings").instance();
if (settingsInstance != null) {
LiveMapInstance settings = settingsInstance.asLiveMap();
// Iterate over key-value pairs.
// Each key is a string, and the value is an Instance for the entry.
for (Map.Entry entry : settings.entries()) {
System.out.println(entry.getKey() + ": " + entry.getValue().compactJson());
}
// Iterate over keys only
for (String key : settings.keys()) { System.out.println("Key: " + key); }
// Iterate over values
for (Instance value : settings.values()) { System.out.println("Value: " + value.compactJson()); }
}
```
Collection methods return empty iterators if the instance is not a `LiveMap`:
#### Javascript
```
const visits = rootObject.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
```
const settings = rootObject.get('settings').instance();
// Get the number of entries
console.log(settings?.size());
```
#### Swift
```
if case .liveMap(let settings)? = try rootObject.get(key: "settings").instance() {
// Get the number of entries - non-optional Int on an Instance
print(try settings.size)
}
```
#### Java
```
Instance settingsInstance = rootObject.get("settings").instance();
if (settingsInstance != null) {
// Get the number of entries - non-null Long on an Instance
System.out.println(settingsInstance.asLiveMap().size());
}
```
The `size()` method returns `undefined` if the instance is not a `LiveMap`:
#### Javascript
```
const visits = rootObject.get('visits').instance(); // This is a LiveCounter, not a LiveMap
console.log(visits?.size()); // undefined
```
### Get a compact object
The Java SDK exposes `compactJson()` as the supported snapshot of an instance; there is no `compact()` equivalent. On an `Instance` the result is non-null; cyclic references are broken via `{"objectId": …}` markers and binary data is base64-encoded.
### Get a compact object
The Swift SDK exposes `compactJson()` as the supported snapshot of an instance; there is no `compact()` equivalent. On an `Instance` the result is a non-optional `JSONValue`; cyclic references are broken via `{"objectId": …}` markers and binary data is base64-encoded.
### Get a compact object
The `compact()` method returns a JavaScript object representation of the instance:
#### Javascript
```
const settingsInstance = rootObject.get('settings').instance();
if (settingsInstance) {
const compact = settingsInstance.compact();
console.log(compact);
// {
// theme: { color: 'dark', fontSize: 14 },
// notifications: true
// }
}
const counterInstance = rootObject.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
```
// Imagine a LiveMap instance has this structure:
// user (LiveMap)
// ├─ profile -> references user (creates cycle)
// └─ name: "Alice"
const userInstance = rootObject.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()`:
Use the `compactJson()` method to obtain a JSON representation of the instance:
#### Javascript
```
// Example: Using the same cyclic structure from before
// user (LiveMap)
// ├─ profile -> references user (creates cycle)
// └─ name: "Alice"
const userInstance = rootObject.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"
// }
}
```
#### Swift
```
// Example: Using the same cyclic structure from before
// user (LiveMap)
// ├─ profile -> references user (creates cycle)
// └─ name: "Alice"
if let userInstance = try rootObject.get(key: "user").instance() {
// compactJson() breaks cycles, making it safe to serialize.
// The cycle is broken via an objectId reference.
print(try userInstance.compactJson()) // non-optional on an Instance
// {"profile":{"objectId":"map:abc123@1234567890"},"name":"Alice"}
}
```
#### Java
```
// Example: Using the same cyclic structure from before
// user (LiveMap)
// ├─ profile -> references user (creates cycle)
// └─ name: "Alice"
Instance userInstance = rootObject.get("user").instance();
if (userInstance != null) {
// compactJson() breaks cycles, making it safe to serialize.
// The cycle is broken via an objectId reference.
System.out.println(userInstance.compactJson()); // non-null on an Instance
// {"profile":{"objectId":"map:abc123@1234567890"},"name":"Alice"}
}
```
Binary data in the channel object are serialized as base64-encoded strings by `compactJson()`:
#### Javascript
```
const rootInstance = rootObject.instance();
if (rootInstance) {
// Store binary data in the LiveMap
const binaryData = new TextEncoder().encode("world");
await rootInstance.set('hello', binaryData);
// compactJson() converts binary data to base64 strings
const compactJson = rootInstance.compactJson();
console.log(compactJson.hello); // "d29ybGQ=" (base64 encoded)
}
```
#### Swift
```
// The channel object always exists, so instance() resolves to a LiveMap here
if case .liveMap(let rootInstance)? = try rootObject.instance() {
// Store binary data in the LiveMap
let binaryData = Data("world".utf8)
try await rootInstance.set(key: "hello", value: .primitive(.data(binaryData)))
// compactJson() converts binary data to base64 strings
let json = try rootInstance.compactJson()
print(json.objectValue?["hello"]?.stringValue ?? "") // "d29ybGQ="
}
```
#### Java
```
// The root LiveMap always exists, so instance() is non-null here
LiveMapInstance rootInstance = rootObject.instance().asLiveMap();
// Store binary data in the LiveMap
rootInstance.set("hello", LiveMapValue.of("world".getBytes(StandardCharsets.UTF_8))).join();
// compactJson() converts binary data to base64 strings;
// LiveMapInstance narrows the return type to JsonObject
JsonObject json = rootInstance.compactJson();
System.out.println(json.get("hello").getAsString()); // "d29ybGQ="
```
## 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](https://ably.com/docs/liveobjects/map.md) for details on these methods.
- For `LiveCounter` instances, you can use methods like `increment()` and `decrement()`. See the [LiveCounter documentation](https://ably.com/docs/liveobjects/counter.md) for details on these methods.
When you call a method on an `Instance`, it directly updates that specific instance.
### Javascript
```
// Update a LiveMap instance
const settings = rootObject.get('settings').instance();
await settings?.set('theme', 'dark');
await settings?.remove('oldSetting');
// Update a LiveCounter instance
const visits = rootObject.get('visits').instance();
await visits?.increment(5);
await visits?.decrement(2);
```
### Swift
```
// Update a LiveMap instance
if case .liveMap(let settings)? = try rootObject.get(key: "settings").instance() {
try await settings.set(key: "theme", value: "dark")
try await settings.remove(key: "oldSetting")
}
// Update a LiveCounter instance
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
try await visits.increment(amount: 5)
try await visits.decrement(amount: 2)
}
```
### Java
```
// Update a LiveMap instance
Instance settingsInstance = rootObject.get("settings").instance();
if (settingsInstance != null) {
LiveMapInstance settings = settingsInstance.asLiveMap();
settings.set("theme", LiveMapValue.of("dark")).join();
settings.remove("oldSetting").join();
}
// Update a LiveCounter instance
Instance visitsInstance = rootObject.get("visits").instance();
if (visitsInstance != null) {
LiveCounterInstance visits = visitsInstance.asLiveCounter();
visits.increment(5).join();
visits.decrement(2).join();
}
```
### 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
```
const settings = rootObject.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](https://ably.com/docs/liveobjects/batch.md) for more details on batching.
## Subscribe to changes
Use the `subscribe()` method to be notified when the instance is updated:
### Javascript
```
const visits = rootObject.get('visits').instance();
if (visits) {
const { unsubscribe } = visits.subscribe(() => {
console.log('Visits updated');
});
// Later, stop listening to changes
unsubscribe();
}
```
### Swift
```
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
let subscription = try visits.subscribe { _ in
print("Visits updated")
}
// Later, stop listening to changes
subscription.unsubscribe()
}
```
### Java
```
Instance visitsInstance = rootObject.get("visits").instance();
if (visitsInstance != null) {
Subscription subscription = visitsInstance.asLiveCounter().subscribe(event ->
System.out.println("Visits updated"));
// Later, stop listening to changes
subscription.unsubscribe();
}
```
Alternatively, use the `subscribeIterator()` method for an async iterator syntax:
### Javascript
```
const visits = rootObject.get('visits').instance();
if (visits) {
for await (const _ of visits.subscribeIterator()) {
console.log('Visits updated');
if (someCondition) {
break; // Unsubscribes
}
}
}
```
Alternatively, use the `events()` method to consume updates as an [`AsyncStream`](https://developer.apple.com/documentation/swift/asyncstream) with `for await`. Breaking out of the loop, or cancelling the enclosing task, automatically unsubscribes:
### Swift
```
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
for await event in try visits.events() {
// event.object wraps the same instance, so read its current value
if case .liveCounter(let updated) = event.object {
print("Visits updated: \(try updated.value)")
}
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](https://ably.com/docs/liveobjects/concepts/objects.md#reachability), subscriptions to that instance are automatically unsubscribed after one final notification:
### Javascript
```
const visits = rootObject.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 rootObject.remove('visits');
}
```
### Swift
```
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
try visits.subscribe { event in
if event.message?.operation.action == .objectDelete {
print("visits counter was deleted")
// This listener is automatically unsubscribed after this event
}
}
// Make the LiveCounter stored in visits unreachable.
// It will eventually be deleted.
try await rootObject.remove(key: "visits")
}
```
### Java
```
Instance visitsInstance = rootObject.get("visits").instance();
if (visitsInstance != null) {
visitsInstance.asLiveCounter().subscribe(event -> {
ObjectMessage message = event.getMessage();
if (message != null && message.getOperation().getAction() == ObjectOperationAction.OBJECT_DELETE) {
System.out.println("visits counter was deleted");
// This listener is automatically unsubscribed after this event
}
});
// Make the LiveCounter stored in visits unreachable.
// It will eventually be deleted.
rootObject.remove("visits").join();
}
```
### 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`](https://ably.com/docs/liveobjects/concepts/operations.md#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made.
- The `object` property contains the same object instance you called `subscribe()` on, as the `Instance` enum. Because it is the instance you already hold, you can read the update straight from your existing binding rather than re-matching the case.
- The `message` property contains the [`ObjectMessage`](https://ably.com/docs/liveobjects/concepts/operations.md#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made. It is optional.
- `getObject()` returns the same object instance you called `subscribe()` on, as the base `Instance` type. Cast it back with the matching `as*` method; the cast is safe here because you already know the type you subscribed on.
- `getMessage()` returns the [`ObjectMessage`](https://ably.com/docs/liveobjects/concepts/operations.md#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made. It is nullable.
#### Javascript
```
const visits = rootObject.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
});
}
```
#### Swift
```
if case .liveCounter(let visits)? = try rootObject.get(key: "visits").instance() {
try visits.subscribe { event in
// event.object is the same instance as visits, so read the new value
// directly from your existing binding
if let value = try? visits.value {
print("New value: \(value)")
}
if let message = event.message {
print("Updated by: \(message.clientId ?? "")")
print("Operation: \(message.operation.action)") // e.g. counterInc
}
}
}
```
#### Java
```
Instance visitsInstance = rootObject.get("visits").instance();
if (visitsInstance != null) {
LiveCounterInstance visits = visitsInstance.asLiveCounter();
visits.subscribe(event -> {
LiveCounterInstance object = event.getObject().asLiveCounter();
System.out.println("New value: " + object.value());
ObjectMessage message = event.getMessage();
if (message != null) {
System.out.println("Updated by: " + message.getClientId());
System.out.println("Operation: " + message.getOperation().getAction()); // e.g. COUNTER_INC
}
System.out.println(object.getId().equals(visits.getId())); // true - same instance
});
}
```
### 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
```
const settings = rootObject.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');
}
```
#### Swift
```
if case .liveMap(let settings)? = try rootObject.get(key: "settings").instance() {
// Subscribe to the settings LiveMap instance
try settings.subscribe { _ in
print("Settings LiveMap updated")
}
// This triggers the subscription (direct change to settings)
try await settings.set(key: "theme", value: "dark")
// This does NOT trigger the subscription (change to nested object)
if case .liveMap(let preferences)? = try settings.get(key: "preferences") {
try await preferences.set(key: "language", value: "en")
}
}
```
#### Java
```
Instance settingsInstance = rootObject.get("settings").instance();
if (settingsInstance != null) {
LiveMapInstance settings = settingsInstance.asLiveMap();
// Subscribe to the settings LiveMap instance
settings.subscribe(event -> System.out.println("Settings LiveMap updated"));
// This triggers the subscription (direct change to settings)
settings.set("theme", LiveMapValue.of("dark")).join();
// This does NOT trigger the subscription (change to nested object)
Instance preferences = settings.get("preferences");
if (preferences != null) {
preferences.asLiveMap().set("language", LiveMapValue.of("en")).join();
}
}
```
## Related Topics
- [Objects](https://ably.com/docs/liveobjects/concepts/objects.md): Learn how data is represented as objects in Ably LiveObjects
- [PathObject](https://ably.com/docs/liveobjects/concepts/path-object.md): Learn about PathObject, a path-based API for accessing and manipulating LiveObjects data structures
- [Operations](https://ably.com/docs/liveobjects/concepts/operations.md): Learn how objects are updated by operations in Ably LiveObjects.
- [Synchronization](https://ably.com/docs/liveobjects/concepts/synchronization.md): Learn how data is synchronized between clients.
## Documentation Index
To discover additional Ably documentation:
1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages.
2. Identify relevant URLs from that index.
3. Fetch target pages as needed.
Avoid using assumed or outdated documentation paths.