# PathObject `PathObject` provides a path-based approach to accessing and manipulating LiveObjects data structures. Instead of working with explicit object instances, you work with paths that resolve to objects dynamically at runtime. A `PathObject` represents a path to a specific location within the channel object. When you call methods on a `PathObject`, they operate on whatever value exists at that path at the time the method is called. ## Get a PathObject Call `channel.object.get()` to obtain a `PathObject` for the root path `""`. This root `PathObject` always resolves to the `LiveMap` at the root of the [channel object](https://ably.com/docs/liveobjects/concepts/objects.md#channel-object), which serves as the entry point for navigation. ### Javascript ``` // Get a PathObject for the channel object const rootObject = await channel.object.get(); // This PathObject has an empty path console.log(rootObject.path()); // "" ``` ### Java ``` // Get a PathObject for the channel object LiveMapPathObject rootObject = channel.object.get().join(); // This PathObject has an empty path System.out.println(rootObject.path()); // "" ``` In the Java SDK the root is already typed as a `LiveMapPathObject`; no cast is needed at the root. Calling `channel.object.get()` implicitly [attaches](https://ably.com/docs/channels/states.md#attach) to the channel if not already attached. The returned promise resolves when the channel object data has been [synchronized](https://ably.com/docs/liveobjects/concepts/synchronization.md) to the client. Calling `channel.object.get()` implicitly [attaches](https://ably.com/docs/channels/states.md#attach) to the channel if not already attached. The returned `CompletableFuture` completes when the channel object data has been [synchronized](https://ably.com/docs/liveobjects/concepts/synchronization.md) to the client. A `PathObject` doesn't hold actual data - it holds a reference to a location. This makes `PathObject` references stable and reusable, even if the underlying object instances change: ### Javascript ``` // Obtain a PathObject at the 'visits' key const visits = rootObject.get('visits'); // Increment the LiveCounter stored at this path await visits.increment(5); // Someone replaces the LiveCounter instance stored at 'visits' await rootObject.set('visits', LiveCounter.create(0)); // The same PathObject can be used to increment the LiveCounter instance // stored at the 'visits' key at the time the method is called await visits.increment(1); ``` ### Java ``` // Obtain a PathObject at the 'visits' key PathObject visits = rootObject.get("visits"); // Increment the LiveCounter stored at this path visits.asLiveCounter().increment(5).join(); // Someone replaces the LiveCounter instance stored at 'visits' LiveCounter replacement = LiveCounter.create(0); rootObject.set("visits", LiveMapValue.of(replacement)).join(); // The same PathObject can be used to increment the LiveCounter instance // stored at the 'visits' key at the time the method is called visits.asLiveCounter().increment(1).join(); ``` A `PathObject` references a location rather than a specific instance, so you can safely store and reuse `PathObject` references throughout your application. ## Type inference When using TypeScript, you can provide type parameters to `channel.object.get()` to get rich type inference: ### Javascript ``` import { type PathObject, LiveCounter, LiveMap } from 'ably/liveobjects'; type RootObject = { visits: LiveCounter; settings: LiveMap<{ theme: string; notifications: boolean; }>; }; const rootObject = await channel.object.get(); // TypeScript knows 'visits' is a LiveCounter const visits: PathObject = rootObject.get('visits'); await visits.increment(1); // Type-safe // TypeScript knows the shape of 'settings' const theme: PathObject = rootObject.get('settings').get('theme'); const value = theme.value(); // Returns string | undefined ``` See the [Typing documentation](https://ably.com/docs/liveobjects/typing.md) for more details on type safety. ## Type inference Java has no user-supplied type parameters. Instead, infer the type of a path by calling one of the `as*` methods, such as `asLiveCounter()` or `asLiveMap()`. These casts never throw, even when the value at the path has a different type: ### Java ``` // Infer the 'visits' path as a LiveCounter and update through it LiveCounterPathObject visits = rootObject.get("visits").asLiveCounter(); visits.increment(1).join(); // A read through the wrong inferred type returns null instead of throwing String theme = rootObject.at("settings.theme").asString().value(); // String or null // Check the type first when it isn't known if (rootObject.get("score").getType() == ValueType.LIVE_COUNTER) { // safe to treat as a counter } ``` A wrong cast only shows up when you use it: reads return `null` or an empty result, and writes return a `CompletableFuture` that completes exceptionally with an `AblyException`. Use `getType()` to check what is stored at a path before inferring its type; it returns a `ValueType`, or `null` when nothing resolves at the path. See the [Type inference documentation](https://ably.com/docs/liveobjects/typing.md#type-inference) for the full contract, including how the instance-layer casts differ. ## Navigate a PathObject `PathObject` provides methods to navigate through the channel object and inspect paths. ### Navigate to child paths Use the `get(key)` method to navigate to a child path. The returned `PathObject` is valid even if nothing exists at that path yet: #### Javascript ``` const rootObject = await channel.object.get(); // Navigate to a child path const settings = rootObject.get('settings'); console.log(settings.path()); // "settings" // Chain get() calls for deeper navigation // get() never returns undefined, even if there is // nothing at the specified path const color = settings.get('theme').get('color'); console.log(color.path()); // "settings.theme.color" console.log(color.value()); // e.g. blue ``` #### Java ``` // Navigate to a child path PathObject settings = rootObject.get("settings"); System.out.println(settings.path()); // "settings" // Chain get() calls for deeper navigation - cast between hops PathObject color = settings.asLiveMap().get("theme").asLiveMap().get("color"); System.out.println(color.path()); // "settings.theme.color" System.out.println(color.asString().value()); // e.g. "blue" ``` ### Navigate using path strings For deeply nested paths, use the `at(path)` method with a dot-separated path string: #### Javascript ``` // Navigate using a path string const color = rootObject.at('settings.theme.color'); console.log(color.path()); // "settings.theme.color" // Equivalent to chained get() calls const same = rootObject.get('settings').get('theme').get('color'); console.log(same.path()); // "settings.theme.color" ``` #### Java ``` // Navigate using a path string PathObject color = rootObject.at("settings.theme.color"); System.out.println(color.path()); // "settings.theme.color" // Equivalent to chained get() calls with casts between hops PathObject same = rootObject.get("settings").asLiveMap().get("theme").asLiveMap().get("color"); System.out.println(same.path()); // "settings.theme.color" ``` When using `at()`, dots (`.`) are treated as path separators. To include a literal dot in a key name, escape it with a backslash (`\.`): #### Javascript ``` // Key contains a dot const apiEndpoint = rootObject.get('config').get('api.endpoint'); console.log(apiEndpoint.path()); // "config.api\.endpoint" // Use the escaped path with at() const same = rootObject.at('config.api\\.endpoint'); console.log(same.path()); // "config.api\.endpoint" ``` #### Java ``` // Key contains a dot PathObject apiEndpoint = rootObject.get("config").asLiveMap().get("api.endpoint"); System.out.println(apiEndpoint.path()); // "config.api\.endpoint" // Use the escaped path with at() - Java string literals double the backslash PathObject same = rootObject.at("config.api\\.endpoint"); System.out.println(same.path()); // "config.api\.endpoint" ``` The `path()` method returns the string representation of the current path, 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 navigate to a nested path within a `LiveMap`. The returned `PathObject` represents whatever exists at that path. What you can do with that `PathObject` depends on whether the path resolves to a primitive value, a `LiveCounter`, or a nested `LiveMap`. ### Read values When an entry contains a primitive value or `LiveCounter`, use the `value()` method to get the current value: When an entry contains a primitive value or `LiveCounter`, infer the matching type first and then call its `value()` method, for example `asString().value()` or `asLiveCounter().value()`: #### Javascript ``` // Get the value of an entry containing a primitive const username = rootObject.get('username'); console.log(username.value()); // e.g. "alice" // Get the value of the LiveCounter stored in 'visits' const visits = rootObject.get('visits'); console.log(visits.value()); // e.g. 5 ``` #### Java ``` // Get the value of an entry containing a primitive System.out.println(rootObject.get("username").asString().value()); // e.g. "alice" // Get the value of the LiveCounter stored in 'visits' System.out.println(rootObject.get("visits").asLiveCounter().value()); // e.g. 5.0 // The same typed-read shape applies to every primitive type System.out.println(rootObject.get("active").asBoolean().value()); // Boolean or null System.out.println(rootObject.get("fontSize").asNumber().value()); // Number or null ``` If the entry doesn't exist, or the entry does not contain a primitive or a `LiveCounter` instance, `value()` returns `undefined`: If the entry doesn't exist, or the entry does not contain a value matching the inferred type, `value()` returns `null`: #### Javascript ``` // Get the value of an entry that doesn't exist const missing = rootObject.get('nonexistent'); console.log(missing.value()); // undefined // Set an entry that contains a nested LiveMap await rootObject.set('settings', LiveMap.create({ theme: 'dark' })); // Calling value() on a LiveMap returns undefined console.log(rootObject.get('settings').value()); // undefined - it's a LiveMap, not a primitive or LiveCounter ``` #### Java ``` // Get the value of an entry that doesn't exist System.out.println(rootObject.get("nonexistent").asString().value()); // null // Calling value() through the wrong inferred type also returns null System.out.println(rootObject.get("settings").asString().value()); // null - it's a LiveMap // exists() distinguishes "no value at this path" from // "a value of a different type" without committing to a typed read System.out.println(rootObject.get("nonexistent").exists()); // false System.out.println(rootObject.get("settings").exists()); // true ``` The Java SDK additionally offers `exists()` (a best-effort presence check) and [`getType()`](https://ably.com/docs/liveobjects/typing.md#get-type) (returns a `ValueType`, or `null` when nothing resolves at the path). Use them to check what is at a path before reading it through an inferred type. ### Obtain object instances To get the specific [`Instance`](https://ably.com/docs/liveobjects/concepts/instance.md) at a path, use the `instance()` method: #### Javascript ``` // Get the LiveCounter instance stored in 'visits' const visits = rootObject.get('visits').instance(); // Work with the specific LiveCounter instance console.log(visits?.id); await visits?.increment(1); // Get the LiveMap instance stored in 'settings' const settings = rootObject.get('settings').instance(); // Work with the specific LiveMap instance console.log(settings?.id); await settings?.set('theme', 'dark'); ``` #### Java ``` // Get the LiveCounter instance stored in 'visits' Instance visitsInstance = rootObject.get("visits").instance(); // Work with the specific LiveCounter instance if (visitsInstance != null) { LiveCounterInstance counter = visitsInstance.asLiveCounter(); // throws if not a counter System.out.println(counter.getId()); counter.increment(1).join(); } // Get the LiveMap instance stored in 'settings' Instance settingsInstance = rootObject.get("settings").instance(); // Work with the specific LiveMap instance if (settingsInstance != null) { LiveMapInstance settings = settingsInstance.asLiveMap(); System.out.println(settings.getId()); settings.set("theme", LiveMapValue.of("dark")).join(); } ``` `instance()` returns `undefined` only if the entry at the path does not exist. A primitive value is wrapped in a read-only `Instance`: In the Java SDK, `instance()` returns `null` only if the entry at the path does not exist. A primitive value is wrapped in a read-only primitive instance: #### Javascript ``` // The 'username' entry contains a primitive string, // so instance() returns a read-only Instance wrapping it const username = rootObject.get('username').instance(); console.log(username?.id); // undefined - primitive instances have no object ID console.log(username?.value()); // e.g. 'alice' // instance() returns undefined only when nothing exists at the path const missing = rootObject.get('nonexistent').instance(); console.log(missing); // undefined ``` #### Java ``` // The 'username' entry contains a primitive string, // so instance() returns a read-only StringInstance Instance usernameInstance = rootObject.get("username").instance(); if (usernameInstance != null) { System.out.println(usernameInstance.asString().value()); // e.g. "alice" } // instance() returns null only when nothing exists at the path Instance missing = rootObject.get("nonexistent").instance(); System.out.println(missing); // null ``` See the [Instance documentation](https://ably.com/docs/liveobjects/concepts/instance.md) for more details on working with object instances. ### Enumerate collections For paths that resolve to a `LiveMap`, you can iterate over the entries, keys, and values: #### Javascript ``` const settings = rootObject.get('settings'); // Iterate over key-value pairs. // Each key is a string, and the value is a PathObject 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()); } ``` #### Java ``` LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); // Iterate over key-value pairs. // Each key is a string, and the value is a PathObject 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 (PathObject value : settings.values()) { System.out.println("Value: " + value.compactJson()); } ``` Collection methods return empty iterators if the path doesn't resolve to a `LiveMap`: #### Javascript ``` const visits = rootObject.get('visits'); // This is a LiveCounter, not a LiveMap for (const [key, value] of visits.entries()) { // This loop doesn't execute - entries() returns empty iterator } ``` #### Java ``` PathObject visits = rootObject.get("visits"); // This is a LiveCounter, not a LiveMap // entries() is empty when the path is not a LiveMap - the cast itself never throws for (Map.Entry entry : visits.asLiveMap().entries()) { // This loop doesn't execute - entries() returns an empty iterable } ``` ### 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'); // Get the number of entries console.log(settings.size()); ``` #### Java ``` LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); // Get the number of entries - returns a Long System.out.println(settings.size()); ``` The `size()` method returns `undefined` if the path doesn't resolve to a `LiveMap`: The `size()` method returns `null` if the path doesn't resolve to a `LiveMap`: #### Javascript ``` const visits = rootObject.get('visits'); // This is a LiveCounter, not a LiveMap console.log(visits.size()); // undefined ``` #### Java ``` Long size = rootObject.get("visits").asLiveMap().size(); // This is a LiveCounter, not a LiveMap System.out.println(size); // null ``` ### Get a compact object The Java SDK exposes `compactJson()` as the supported snapshot of the data at a path; there is no `compact()` equivalent. `compactJson()` returns a `JsonElement` that is always safe to serialize: 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 data at a path: #### Javascript ``` const settings = rootObject.get('settings'); const compact = settings.compact(); console.log(compact); // { // theme: { color: 'dark', fontSize: 14 }, // notifications: true // } const visits = rootObject.get('visits'); console.log(visits.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 channel object contains cyclic references, these are preserved in the returned data structure: #### Javascript ``` // Imagine the channel object has this structure: // root (LiveMap) // ├─ user (LiveMap) // │ └─ profile -> references root (creates cycle) // └─ name: "Alice" // When you call compact(), cyclic references are preserved as actual JS references const root = rootObject.compact(); console.log(root.user.profile === root); // true - actual cyclic reference // You can navigate the cycle console.log(root.user.profile.user.profile.name); // "Alice" // This will throw an error because cycles can't be serialized JSON.stringify(compact); // ❌ 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 data at a path. On the path layer it returns `null` when the path doesn't resolve: #### Javascript ``` // Example: Using the same cyclic structure from before // root (LiveMap) // ├─ user (LiveMap) // │ └─ profile -> references root (creates cycle) // └─ name: "Alice" // compactJson() breaks cycles, making it safe to serialize const compactJson = rootObject.compactJson(); console.log(compactJson); // This can be safely serialized as the // cycle is broken via an objectId reference console.log(JSON.stringify(compactJson)); // { // "user": { // "profile": { "objectId": "root" } // }, // "name": "Alice" // } ``` #### Java ``` // Example: Using the same cyclic structure from before // root (LiveMap) // ├─ user (LiveMap) // │ └─ profile -> references root (creates cycle) // └─ name: "Alice" // compactJson() breaks cycles, making it safe to serialize JsonElement compactJson = rootObject.compactJson(); // null if the path doesn't resolve System.out.println(compactJson); // {"user":{"profile":{"objectId":"root"}},"name":"Alice"} <- cycle broken via objectId marker ``` Binary data in the channel object are serialized as base64-encoded strings by `compactJson()`: #### Javascript ``` // Store binary data in the channel object const binaryData = new TextEncoder().encode("world"); await rootObject.set('hello', binaryData); // compactJson() converts binary data to base64 strings const compactJson = rootObject.compactJson(); console.log(compactJson.hello); // "d29ybGQ=" (base64 encoded) ``` #### Java ``` // Store binary data in the channel object byte[] binaryData = "world".getBytes(StandardCharsets.UTF_8); rootObject.set("hello", LiveMapValue.of(binaryData)).join(); // compactJson() converts binary data to base64 strings System.out.println(((JsonObject) rootObject.compactJson()).get("hello").getAsString()); // "d29ybGQ=" ``` ## Update data `PathObject` provides mutation methods that operate on the object at the resolved path. The specific methods available depend on the type of object at that path: - For paths that resolve to a `LiveMap`, 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 paths that resolve to a `LiveCounter`, 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 a `PathObject`, the path is resolved to a specific instance at the time the method is called, and the operation is performed on that instance. ### Javascript ``` // Update a LiveMap const settings = rootObject.get('settings'); await settings.set('theme', 'dark'); await settings.remove('oldSetting'); // Update a LiveCounter const visits = rootObject.get('visits'); await visits.increment(5); await visits.decrement(2); ``` ### Java ``` // Update a LiveMap LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); settings.set("theme", LiveMapValue.of("dark")).join(); settings.remove("oldSetting").join(); // Update a LiveCounter LiveCounterPathObject visits = rootObject.get("visits").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 resolved to the specific object instance at the path where `batch()` is called: #### Javascript ``` const settings = rootObject.get('settings'); 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 object data is updated: ### Javascript ``` const visits = rootObject.get('visits'); const { unsubscribe } = visits.subscribe(() => { console.log('Visits updated'); }); // Later, stop listening to changes unsubscribe(); ``` ### Java ``` PathObject visits = rootObject.get("visits"); Subscription subscription = visits.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'); for await (const _ of visits.subscribeIterator()) { console.log('Visits updated'); if (someCondition) { break; // Unsubscribes } } ``` The Java SDK has no `subscribeIterator()` equivalent; a listener plus the returned `Subscription` is the only subscription form. `PathObject` subscriptions observe a location rather than a specific object instance. If the object at the specified path is replaced, the subscription automatically continues to observe the new instance: ### Javascript ``` const visits = rootObject.get('visits'); // Subscribe to the 'visits' path visits.subscribe(() => { console.log('Visits updated'); }); // This triggers the subscription await visits.increment(5); // Someone replaces the LiveCounter instance at 'visits' await rootObject.set('visits', LiveCounter.create(100)); // This triggers the subscription, which now observes the new LiveCounter await visits.increment(1); ``` ### Java ``` PathObject visits = rootObject.get("visits"); // Subscribe to the 'visits' path visits.subscribe(event -> System.out.println("Visits updated")); // This triggers the subscription visits.asLiveCounter().increment(5).join(); // Someone replaces the LiveCounter instance at 'visits' LiveCounter newCounter = LiveCounter.create(100); rootObject.set("visits", LiveMapValue.of(newCounter)).join(); // This triggers the subscription, which now observes the new LiveCounter visits.asLiveCounter().increment(1).join(); ``` You can subscribe to any path, including those containing primitive values: ### Javascript ``` const theme = rootObject.get("settings").get('theme'); theme.subscribe(() => { console.log('Theme updated:', theme.value()); }); await rootObject.get("settings").set("theme", "dark"); ``` ### Java ``` PathObject theme = rootObject.get("settings").asLiveMap().get("theme"); theme.subscribe(event -> System.out.println("Theme updated: " + theme.asString().value())); rootObject.get("settings").asLiveMap().set("theme", LiveMapValue.of("dark")).join(); ``` ### Determine what changed The subscription receives an argument with information about the update: - The `object` field contains a `PathObject` representing the location of the object instance that was updated. - 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. - `getObject()` returns a `PathObject` pointing to the path where the change occurred. - `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'); visits.subscribe(({ object, message }) => { console.log('New value:', object.value()); console.log('Updated by:', message?.clientId); console.log('Operation:', message?.operation.action); }); ``` #### Java ``` PathObject visits = rootObject.get("visits"); visits.subscribe(event -> { System.out.println("New value: " + event.getObject().asLiveCounter().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 } }); ``` When subscribed to a `LiveCounter`, the `object` passed to the subscription is always a `PathObject` for the same path you subscribed to, since there can be no further nesting: #### Javascript ``` const visits = rootObject.get('visits'); visits.subscribe(({ object }) => { console.log(object.path()); // always "visits" }); await visits.increment(1); ``` #### Java ``` PathObject visits = rootObject.get("visits"); visits.subscribe(event -> System.out.println(event.getObject().path())); // always "visits" visits.asLiveCounter().increment(1).join(); ``` When subscribed to a `LiveMap`, the `object` passed to the subscription is a `PathObject` to the location of the `LiveMap`, `LiveCounter`, or primitive value that was updated: #### Javascript ``` // Subscribe to a LiveCounter stored in 'visits' const visits = rootObject.get('visits'); visits.subscribe(({ object, message }) => { console.log('path:', object.path(), 'number:', message?.operation?.counterInc?.number); }); await visits.increment(5); // path: visits number: 5 // Subscribe to a LiveMap stored in 'settings' const settings = rootObject.get('settings'); settings.subscribe(({ object, message }) => { console.log('path:', object.path(), 'key:', message?.operation?.mapSet?.key); }); await settings.set('theme', 'dark'); // path: settings key: theme await settings.get('preferences').set('language', 'en'); // path: settings.preferences key: language // Subscribe to the 'theme' key in the LiveMap stored in 'settings' const theme = settings.get('theme'); theme.subscribe(({ object, message }) => { console.log('path:', object.path(), 'key:', message?.operation?.mapSet?.key); }); await settings.set('theme', 'dark'); // path: settings.theme key: theme ``` #### Java ``` // Subscribe to a LiveCounter stored in 'visits' PathObject visits = rootObject.get("visits"); visits.subscribe(event -> { ObjectMessage message = event.getMessage(); // getCounterInc() is non-null only for counter increment operations CounterInc counterInc = message != null ? message.getOperation().getCounterInc() : null; if (counterInc != null) { System.out.println("path: " + event.getObject().path() + " number: " + counterInc.getNumber()); } }); visits.asLiveCounter().increment(5).join(); // path: visits number: 5.0 // Subscribe to a LiveMap stored in 'settings' LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); settings.subscribe(event -> { ObjectMessage message = event.getMessage(); // getMapSet() is non-null only for map set operations MapSet mapSet = message != null ? message.getOperation().getMapSet() : null; if (mapSet != null) { System.out.println("path: " + event.getObject().path() + " key: " + mapSet.getKey()); } }); settings.set("theme", LiveMapValue.of("dark")).join(); // path: settings key: theme settings.get("preferences").asLiveMap().set("language", LiveMapValue.of("en")).join(); // path: settings.preferences key: language // Subscribe to the 'theme' key in the LiveMap stored in 'settings' PathObject theme = settings.get("theme"); theme.subscribe(event -> { ObjectMessage message = event.getMessage(); MapSet mapSet = message != null ? message.getOperation().getMapSet() : null; if (mapSet != null) { System.out.println("path: " + event.getObject().path() + " key: " + mapSet.getKey()); } }); settings.set("theme", LiveMapValue.of("dark")).join(); // path: settings.theme key: theme ``` Since the path of the `object` is dynamic, read from a known `PathObject` inside the subscription to access the latest values: #### Javascript ``` const settings = rootObject.get('settings'); settings.subscribe(() => { console.log("Theme:", settings.get("theme").value()); console.log("Preferences:", settings.get("preferences").compactJson()); }); await settings.set('theme', 'dark'); await settings.get('preferences').set('language', 'en'); ``` #### Java ``` LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); settings.subscribe(event -> { System.out.println("Theme: " + settings.get("theme").asString().value()); System.out.println("Preferences: " + settings.get("preferences").compactJson()); }); settings.set("theme", LiveMapValue.of("dark")).join(); settings.get("preferences").asLiveMap().set("language", LiveMapValue.of("en")).join(); ``` ### Control subscription depth By default, subscriptions observe changes at all nested levels. Use the `depth` option to limit how deep the subscription listens: #### Javascript ``` const settings = rootObject.get('settings'); // Only observe direct changes to the settings LiveMap // Changes to any nested objects are ignored settings.subscribe(({ object }) => { console.log('Settings updated'); console.log('Changed path:', object.path()); // Always "settings" }, { depth: 1 }); ``` #### Java ``` LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); // Only observe direct changes to the settings LiveMap // Changes to any nested objects are ignored settings.subscribe(event -> { System.out.println("Settings updated"); System.out.println("Changed path: " + event.getObject().path()); // Always "settings" }, new PathObjectSubscriptionOptions(1)); // depth 1: direct children only ``` Create `PathObjectSubscriptionOptions` with no arguments to observe changes at all depths. The depth passed to the constructor must be 1 or greater; zero or negative values are rejected with an `AblyException` (status `400`, error code `40003`). The `depth` option also works with async iterators: #### Javascript ``` for await (const { object } of settings.subscribeIterator({ depth: 1 })) { console.log('Settings LiveMap updated'); console.log('Changed path:', object.path()); // Always "settings" } ``` ## Related Topics - [Objects](https://ably.com/docs/liveobjects/concepts/objects.md): Learn how data is represented as objects in Ably LiveObjects - [Instance](https://ably.com/docs/liveobjects/concepts/instance.md): Learn about Instance, a reference to a specific LiveObject instance for direct manipulation - [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.