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