# 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), which operates on paths that resolve dynamically at runtime, an `Instance` is bound to a specific object instance identified by its object ID. An `Instance` represents a specific object instance within the channel object. When you call methods on an `Instance`, they operate on that specific instance regardless of where it exists in the structure or whether it has been moved. ## Get an Instance Obtain an `Instance` from a `PathObject` using the `instance()` method: ```javascript // Get a PathObject for the channel object const myObject = await channel.object.get(); // Get the specific Instance of a LiveCounter located at the 'visits' key const visits = myObject.get('visits').instance(); console.log(counterInstance?.id); // e.g. counter:abc123@1234567890 console.log(counterInstance?.value()); // e.g. 5 ``` The `instance()` method returns `undefined` if no object exists at that path. Once you have an `Instance`, it references a specific object by its ID. This means operations target that exact object: ```javascript // Obtain an Instance for a LiveCounter const visits = myObject.get('visits').instance(); // Increment the specific LiveCounter instance await visits?.increment(5); ``` An `Instance` references a specific object by its ID rather than by location. ## Type inference When using TypeScript, you can provide type parameters to `instance()` to get rich type inference: ```javascript import { type Instance, LiveCounter, LiveMap } from 'ably/liveobjects'; type Settings = { theme: string; notifications: boolean; }; type MyObject = { visits: LiveCounter; settings: LiveMap; }; const myObject = await channel.object.get(); // TypeScript knows 'visits' is a LiveCounter const visits: Instance | undefined = myObject.get('visits').instance(); if (visits) { await visits.increment(1); // Type-safe } // TypeScript knows the shape of 'settings' const settings: Instance> | undefined = myObject.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) for more details on type safety. ## Navigate an Instance For `LiveMap` instances, use the `get(key)` method to navigate to a child value: ```javascript const myObject = await channel.object.get(); // Obtain an instance const settings = myObject.get('settings').instance(); console.log(settings?.id); // e.g. "map:abc123@1234567890" // Chain get() calls for deeper navigation // get() may return undefined at any point if the instance // is not a LiveMap or the accessed entry does not exist const color = settings?.get('theme')?.get('color'); console.log(color?.value()); // e.g. blue ``` The `id` getter returns the [object ID](https://ably.com/docs/liveobjects/concepts/objects#object-ids) of the instance, which can be used with the [REST API](https://ably.com/docs/liveobjects/rest-api-usage). ## 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 = myObject.get('user').instance(); // Get the value of an entry containing a primitive const username = user?.get('username'); console.log('Username:', username?.value()); // e.g. "alice" // Get the value of the LiveCounter stored in 'visits' const visits = user?.get('visits'); console.log(visits?.value()); // e.g. 5 ``` If the entry doesn't exist, `get()` returns undefined: ```javascript const user = myObject.get('user').instance(); if (user) { // Get an entry that doesn't exist on the instance console.log(user.get('nonexistent')); // undefined } ``` If the entry exists, but does not contain a primitive or a `LiveCounter` instance, `value()` returns `undefined`: ```javascript const myObject = await channel.object.get(); // Set an entry that contains a nested LiveMap await myObject.set('settings', LiveMap.create({ theme: 'dark' })); // Calling value() on a LiveMap returns undefined const settings = myObject.get('settings').instance(); console.log(settings?.value()); // undefined - it's a LiveMap, not a primitive or LiveCounter ``` ### Enumerate collections For `LiveMap` instances, you can iterate over the entries, keys, and values: ```javascript const settings = myObject.get('settings').instance(); if (settings) { // Iterate over key-value pairs. // Each key is a string, and the value is an Instance for the entry. for (const [key, value] of settings.entries()) { console.log(`${key}:`, value.value()); } // Iterate over keys only for (const key of settings.keys()) { console.log('Key:', key); } // Iterate over values for (const value of settings.values()) { console.log('Value:', value.value()); } } ``` Collection methods return empty iterators if the instance is not a `LiveMap`: ```javascript const visits = myObject.get('visits').instance(); // This is a LiveCounter, not a LiveMap if (visits) { for (const [key, value] of visits.entries()) { // This loop doesn't execute - entries() returns empty iterator } } ``` ### Get the size of a collection Use the `size()` method to get the number of entries in a `LiveMap`: ```javascript const settings = myObject.get('settings').instance(); // Get the number of entries console.log(settings?.size()); ``` The `size()` method returns `undefined` if the instance is not a `LiveMap`: ```javascript const visits = myObject.get('visits').instance(); // This is a LiveCounter, not a LiveMap console.log(visits?.size()); // undefined ``` ### Get a compact object The `compact()` method returns a JavaScript object representation of the instance: ```javascript const settingsInstance = myObject.get('settings').instance(); if (settingsInstance) { const compact = settingsInstance.compact(); console.log(compact); // { // theme: { color: 'dark', fontSize: 14 }, // notifications: true // } } const counterInstance = myObject.get('visits').instance(); if (counterInstance) { console.log(counterInstance.compact()); // Just the number, e.g., 42 } ``` Binary data in the channel object (such as `ArrayBuffer`/`Uint8Array` in browser environments, or `Buffer` in Node.js) is preserved as typed arrays in the returned object. If the instance contains cyclic references, these are preserved in the returned data structure: ```javascript // Imagine a LiveMap instance has this structure: // user (LiveMap) // ├─ profile -> references user (creates cycle) // └─ name: "Alice" const userInstance = myObject.get('user').instance(); if (userInstance) { // When you call compact(), cyclic references are preserved as actual JS references const compacted = userInstance.compact(); console.log(compacted.profile === compacted); // true - actual cyclic reference // You can navigate the cycle console.log(compacted.profile.profile.name); // "Alice" // This will throw an error because cycles can't be serialized JSON.stringify(compacted); // ❌ TypeError: cyclic object value } ``` It is possible for the value returned from `compact()` to contain cyclic references, so it is not safe to serialize this value with `JSON.stringify()`. Use the `compactJson()` method to obtain a value that can be safely passed to `JSON.stringify()`: ```javascript // Example: Using the same cyclic structure from before // user (LiveMap) // ├─ profile -> references user (creates cycle) // └─ name: "Alice" const userInstance = myObject.get('user').instance(); if (userInstance) { // compactJson() breaks cycles, making it safe to serialize const compactJson = userInstance.compactJson(); console.log(compactJson); // This can be safely serialized as the // cycle is broken via an objectId reference console.log(JSON.stringify(compactJson)); // { // "profile": { "objectId": "map:abc123@1234567890" }, // "name": "Alice" // } } ``` Binary data in the channel object are serialized as base64-encoded strings by `compactJson()`: ```javascript const mapInstance = myObject.instance(); if (mapInstance) { // Store binary data in the LiveMap const binaryData = new TextEncoder().encode("world"); await mapInstance.set('hello', binaryData); // compactJson() converts binary data to base64 strings const compactJson = mapInstance.compactJson(); console.log(compactJson.hello); // "d29ybGQ=" (base64 encoded) } ``` ## Update data `Instance` provides mutation methods that operate on the specific instance. The specific methods available depend on the type of the instance: - For `LiveMap` instances, you can use methods like `set()` and `remove()`. See the [LiveMap documentation](https://ably.com/docs/liveobjects/map) 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) 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 = myObject.get('settings').instance(); await settings?.set('theme', 'dark'); await settings?.remove('oldSetting'); // Update a LiveCounter instance const visits = myObject.get('visits').instance(); await visits?.increment(5); await visits?.decrement(2); ``` ### Batch multiple updates The `batch(callback)` method groups multiple mutations into a single message. The `ctx` parameter is the batch context for the specific instance: ```javascript const settings = myObject.get('settings').instance(); await settings?.batch((ctx) => { ctx.set('theme', 'dark'); ctx.get('preferences')?.set('language', 'en'); ctx.get('volume')?.increment(5); }); ``` See the [Batch operations documentation](https://ably.com/docs/liveobjects/batch) for more details on batching. ## Subscribe to changes Use the `subscribe()` method to be notified when the instance is updated: ```javascript const visits = myObject.get('visits').instance(); if (visits) { const { unsubscribe } = visits.subscribe(() => { console.log('Visits updated'); }); // Later, stop listening to changes unsubscribe(); } ``` Alternatively, use the `subscribeIterator()` method for an async iterator syntax: ```javascript const visits = myObject.get('visits').instance(); if (visits) { for await (const _ of visits.subscribeIterator()) { console.log('Visits updated'); if (someCondition) { break; // Unsubscribes } } } ``` `Instance` subscriptions observe a specific object instance rather than a location. If the instance is moved to a different location within the channel object, the subscription continues to observe the same instance. When an object instance is deleted after becoming [unreachable](https://ably.com/docs/liveobjects/concepts/objects?lang=javascript#reachability), subscriptions to that instance are automatically unsubscribed after one final notification: ```javascript const visits = myObject.get('visits').instance(); if (visits) { visits.subscribe(({ object, message }) => { if (message?.operation.action === 'object.delete') { console.log('visits counter was deleted'); // This listener is automatically unsubscribed after this call } }); // Make the LiveCounter stored in visits unreachable. // It will eventually be deleted. await myObject.remove('visits'); } ``` ### Determine what changed The subscription receives an argument with information about the update: - The `object` field contains an `Instance` representing the same object instance you called `subscribe()` on - The `message` field contains the [`ObjectMessage`](https://ably.com/docs/liveobjects/concepts/operations#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made. ```javascript const visits = myObject.get('visits').instance(); if (visits) { visits.subscribe(({ object, message }) => { console.log('New value:', object.value()); console.log('Updated by:', message?.clientId); console.log('Operation:', message?.operation.action); console.log(object.id === visits.id); // true }); } ``` ### Updates to nested objects Instance subscriptions only observe changes to the specific instance that is subscribed. If an update is made to a nested child object, that is not surfaced on an instance subscription: ```javascript const settings = myObject.get('settings').instance(); if (settings) { // Subscribe to the settings LiveMap instance settings.subscribe(({ object }) => { console.log('Settings LiveMap updated'); }); // This triggers the subscription (direct change to settings) await settings.set('theme', 'dark'); // This does NOT trigger the subscription (change to nested object) await settings.get('preferences')?.set('language', 'en'); } ```