# Objects LiveObjects enables you to store shared data as "objects" on a channel, allowing your application data to be synchronized across multiple users and devices in realtime. This document explains the key concepts you need to know when working with objects. ## Primitive types Primitive types are the fundamental data types that can be stored in a collection type. Currently, the only supported collection type is a [LiveMap](https://ably.com/docs/liveobjects/map.md). LiveObjects supports the following primitive types: * `string` * `number` * `boolean` * `bytes` * JSON arrays or objects * `String` * `Double` * `Bool` * `Data` * JSON arrays or objects * `String` * `Number` (Double, Integer, Float, etc.) * `Boolean` * `byte[]` (binary data) * JSON arrays or objects (`JsonArray`, `JsonObject`) ## Object types LiveObjects provides specialized object types to model your application state. These object types are designed to be conflict-free and eventually consistent, meaning that all operations on them are commutative and converge to the same state across all clients. ### LiveMap [LiveMap](https://ably.com/docs/liveobjects/map.md) is a key/value data structure similar to a dictionary or JavaScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) `Dictionary`Java `Map`: * Keys must be strings * Values can be primitive types, JSON-serializable objects or arrays, or [references](#composability) to other objects * Supports `set` and `remove` operations * Concurrent updates to the same key are resolved using last-write-wins (LWW) semantics #### Javascript ``` // Create and assign a LiveMap await rootObject.set('settings', LiveMap.create({ theme: 'dark', notifications: true })); // Access via path const settings = rootObject.get('settings'); await settings.set('fontSize', 14); ``` #### Swift ``` // rootObject obtained from channel.object.get() // Create a LiveMap with initial entries let initialSettings = LiveMap.create(entries: [ "theme": "dark", "notifications": true, ]) // Assign it to the 'settings' key on the channel object try await rootObject.set(key: "settings", value: .liveMap(initialSettings)) // Access and update it through its path let settings = rootObject.get(key: "settings").asLiveMap() try await settings.set(key: "fontSize", value: 14) ``` #### Java ``` // rootObject obtained from channel.object.get() // Create a LiveMap with initial entries LiveMap initialSettings = LiveMap.create(Map.of( "theme", LiveMapValue.of("dark"), "notifications", LiveMapValue.of(true))); // Assign it to the 'settings' key on the channel object rootObject.set("settings", LiveMapValue.of(initialSettings)).join(); // Access and update it through its path LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); settings.set("fontSize", LiveMapValue.of(14)).join(); ``` ### LiveCounter [LiveCounter](https://ably.com/docs/liveobjects/counter.md) is a numeric counter type: * The value is a double-precision floating-point number * Supports `increment` and `decrement` operations #### Javascript ``` // Create and assign a LiveCounter await rootObject.set('visits', LiveCounter.create(0)); // Access via path const visits = rootObject.get('visits'); await visits.increment(1); ``` #### Swift ``` // Create a LiveCounter with initial value 0 let visitsCounter = LiveCounter.create(initialCount: 0) // Assign it to the 'visits' key on the channel object try await rootObject.set(key: "visits", value: .liveCounter(visitsCounter)) // Access and update it through its path let visits = rootObject.get(key: "visits").asLiveCounter() try await visits.increment(amount: 1) ``` #### Java ``` // Create a LiveCounter with initial value 0 LiveCounter visitsCounter = LiveCounter.create(0); // Assign it to the 'visits' key on the channel object rootObject.set("visits", LiveMapValue.of(visitsCounter)).join(); // Access and update it through its path LiveCounterPathObject visits = rootObject.get("visits").asLiveCounter(); visits.increment(1).join(); ``` ## Channel object The channel object is a special `LiveMap` instance which: * Implicitly exists on a channel and does not need to be created explicitly * Has the special [objectId](#object-ids) of `root` * Cannot be deleted * Serves as the [entry point](#reachability) for accessing all other objects on a channel Access the channel object using `channel.object.get()`, which returns a [`PathObject`](https://ably.com/docs/liveobjects/concepts/path-object.md) that resolves to the root `LiveMap`: Access the channel object using `channel.object.get()`, which returns a [`LiveMapPathObject`](https://ably.com/docs/liveobjects/concepts/path-object.md) resolving to the root `LiveMap`. The `channel.object` accessor is available once you register the LiveObjects plugin through `clientOptions.plugins`: Access the channel object using `channel.object.get()`, which returns a future that completes with a [`LiveMapPathObject`](https://ably.com/docs/liveobjects/concepts/path-object.md) resolving to the root `LiveMap`: ### Javascript ``` // Get the channel object. // This implicitly attaches to the channel if not already attached. // The promise resolves when the channel object data has been synchronized to the client. const rootObject = await channel.object.get(); // Use it like any other LiveMap await rootObject.set('app-version', '1.0.0'); await rootObject.set('config', LiveMap.create({ apiUrl: 'https://api.example.com' })); ``` ### Swift ``` // Implicitly attaches the channel; suspends until the state is synchronized let rootObject = try await channel.object.get() // Use it like any other LiveMap try await rootObject.set(key: "app-version", value: "1.0.0") // Create a config map let config = LiveMap.create(entries: [ "apiUrl": "https://api.example.com", ]) // Assign it to the 'config' key on the channel object try await rootObject.set(key: "config", value: .liveMap(config)) ``` ### Java ``` // Implicitly attaches the channel; the future completes once state is SYNCED LiveMapPathObject rootObject = channel.object.get().join(); // Use it like any other LiveMap rootObject.set("app-version", LiveMapValue.of("1.0.0")).join(); // Create a config map LiveMap config = LiveMap.create(Map.of( "apiUrl", LiveMapValue.of("https://api.example.com"))); // Assign it to the 'config' key on the channel object rootObject.set("config", LiveMapValue.of(config)).join(); ``` ## PathObject [`PathObject`](https://ably.com/docs/liveobjects/concepts/path-object.md) provides a path-based API for interacting with objects at specific locations. The path is evaluated when a method is called, resolving to whatever instance exists at that location: ### Javascript ``` // Get the channel object const rootObject = await channel.object.get(); // Navigate to nested paths const theme = rootObject.get('settings').get('theme'); // The path evaluates to an object instance when you call a method on it await rootObject.get('visits').increment(5); await rootObject.get('settings').set('notifications', true); ``` ### Swift ``` // Get the channel object let rootObject = try await channel.object.get() // Purely navigational - nothing is resolved yet let visits = rootObject.get(key: "visits") // Resolution happens when a terminal method is called try await visits.asLiveCounter().increment(amount: 5) try await rootObject.get(key: "settings").asLiveMap().set(key: "notifications", value: true) ``` ### Java ``` // Get the channel object LiveMapPathObject rootObject = channel.object.get().join(); // Purely navigational - nothing is resolved yet PathObject visits = rootObject.get("visits"); // Resolution happens when a terminal method is called visits.asLiveCounter().increment(5).join(); rootObject.get("settings").asLiveMap().set("notifications", LiveMapValue.of(true)).join(); ``` Use `PathObject` for: - Location-based operations (most common use case) - Resilient code that works regardless of instance replacements - Simple navigation through your data structure See the [PathObject documentation](https://ably.com/docs/liveobjects/concepts/path-object.md) for details. ## Instance [`Instance`](https://ably.com/docs/liveobjects/concepts/instance.md) provides direct access to a specific object instance, which is identified by a unique [object ID](https://ably.com/docs/liveobjects/concepts/objects.md#object-ids). ### Javascript ``` // Get the channel object const rootObject = await channel.object.get(); // Navigate to nested paths and get the instance const themeInstance = rootObject.get('settings').get('theme').instance(); // undefined only if nothing exists at the path // Work directly with the instance await rootObject.get('visits').instance()?.increment(5); await rootObject.get('settings').instance()?.set('notifications', true); ``` ### Swift ``` // Get the channel object let rootObject = try await channel.object.get() // Navigate to nested paths and get the instance let themeInstance = try rootObject.get(key: "settings").asLiveMap().get(key: "theme").instance() // Work directly with the instance. // instance() returns nil only if nothing exists at the path if case .liveCounter(let visitsInstance)? = try rootObject.get(key: "visits").instance() { try await visitsInstance.increment(amount: 5) } if case .liveMap(let settingsInstance)? = try rootObject.get(key: "settings").instance() { try await settingsInstance.set(key: "notifications", value: true) } ``` ### Java ``` // Get the channel object LiveMapPathObject rootObject = channel.object.get().join(); // Navigate to nested paths and get the instance Instance themeInstance = rootObject.get("settings").asLiveMap().get("theme").instance(); // Work directly with the instance Instance visitsInstance = rootObject.get("visits").instance(); // null only if nothing exists at the path if (visitsInstance != null) { visitsInstance.asLiveCounter().increment(5).join(); } Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { settingsInstance.asLiveMap().set("notifications", LiveMapValue.of(true)).join(); } ``` Use `Instance` for: - Tracking specific objects as they move through your data structure - Detecting when a specific object is deleted See the [Instance documentation](https://ably.com/docs/liveobjects/concepts/instance.md) for details. ## Composability LiveObjects enables you to build complex, hierarchical data structures through composability. Specifically, a [LiveMap](https://ably.com/docs/liveobjects/map.md) can store references to other `LiveMap` or `LiveCounter` object instances as values. This allows you to create nested hierarchies of data. ### Javascript ``` // Create a nested structure, published atomically in a single message await rootObject.set('profile', LiveMap.create({ name: 'Alice', preferences: LiveMap.create({ theme: 'dark', fontSize: 14 }), activity: LiveCounter.create(0) })); // Resulting structure: // rootObject (LiveMap - channel object) // └── profile (LiveMap) // ├── name: "Alice" (string) // ├── preferences (LiveMap) // │ ├── theme: "dark" (string) // │ └── fontSize: 14 (number) // └── activity (LiveCounter) // Access and update nested values via paths await rootObject.get('profile').get('activity').increment(5); await rootObject.get('profile').get('preferences').set('theme', 'light'); ``` ### Swift ``` // Create a map for the nested preferences let preferences = LiveMap.create(entries: [ "theme": "dark", "fontSize": 14, ]) // Create a counter for the nested activity let activity = LiveCounter.create(initialCount: 0) // Compose the profile entries, referencing the nested objects let profile = LiveMap.create(entries: [ "name": "Alice", "preferences": .liveMap(preferences), "activity": .liveCounter(activity), ]) // Assign the profile map to the channel object. // The whole tree is published atomically in a single message (one operation per created object plus the final set). try await rootObject.set(key: "profile", value: .liveMap(profile)) // Resulting structure: // rootObject (LiveMap - channel object) // └── profile (LiveMap) // ├── name: "Alice" (string) // ├── preferences (LiveMap) // │ ├── theme: "dark" (string) // │ └── fontSize: 14 (number) // └── activity (LiveCounter) // Update nested values through their paths try await rootObject.at(path: "profile.activity").asLiveCounter().increment(amount: 5) try await rootObject.at(path: "profile.preferences").asLiveMap().set(key: "theme", value: "light") ``` ### Java ``` // Create a map for the nested preferences LiveMap preferences = LiveMap.create(Map.of( "theme", LiveMapValue.of("dark"), "fontSize", LiveMapValue.of(14))); // Create a counter for the nested activity LiveCounter activity = LiveCounter.create(0); // Compose the profile entries, referencing the nested objects Map profileData = Map.of( "name", LiveMapValue.of("Alice"), "preferences", LiveMapValue.of(preferences), "activity", LiveMapValue.of(activity)); // Create the profile map from the composed entries LiveMap profile = LiveMap.create(profileData); // Assign the profile map to the channel object. // The whole tree is published atomically in a single message (one operation per created object plus the final set). rootObject.set("profile", LiveMapValue.of(profile)).join(); // Resulting structure: // rootObject (LiveMap - channel object) // └── profile (LiveMap) // ├── name: "Alice" (string) // ├── preferences (LiveMap) // │ ├── theme: "dark" (string) // │ └── fontSize: 14 (number) // └── activity (LiveCounter) // Update nested values through their paths rootObject.at("profile.activity").asLiveCounter().increment(5).join(); rootObject.at("profile.preferences").asLiveMap().set("theme", LiveMapValue.of("light")).join(); ``` ## Reachability All objects must be reachable from the channel object (directly or indirectly). Objects that cannot be reached from the channel object will eventually [be deleted](https://ably.com/docs/liveobjects/lifecycle.md#objects-deleted). When you replace or remove an object reference, it may become unreachable and will eventually be [deleted](https://ably.com/docs/liveobjects/lifecycle.md#objects-deleted): ### Javascript ``` // Create and assign a counter await rootObject.set('visits', LiveCounter.create(0)); // Get the counter instance to track it const oldCounter = rootObject.get('visits').instance(); if (oldCounter) { oldCounter.subscribe(({ object, message }) => { if (message?.operation.action === 'object.delete') { console.log('Counter was deleted'); } }); // Replace with a new counter - old counter becomes unreachable await rootObject.set('visits', LiveCounter.create(0)); // The subscription will fire with the delete notification } ``` ### Swift ``` // Create and assign a counter let visitsCounter = LiveCounter.create(initialCount: 0) try await rootObject.set(key: "visits", value: .liveCounter(visitsCounter)) // Get the counter instance to track it if case .liveCounter(let oldCounter)? = try rootObject.get(key: "visits").instance() { try oldCounter.subscribe { event in if event.message?.operation.action == .objectDelete { print("Old counter was deleted") } } } // Replace with a new counter - the old instance becomes unreachable let replacementCounter = LiveCounter.create(initialCount: 0) try await rootObject.set(key: "visits", value: .liveCounter(replacementCounter)) // The subscription will fire with the delete notification ``` ### Java ``` // Create and assign a counter LiveCounter visitsCounter = LiveCounter.create(0); rootObject.set("visits", LiveMapValue.of(visitsCounter)).join(); // Get the counter instance to track it Instance oldCounter = rootObject.get("visits").instance(); if (oldCounter != null) { oldCounter.asLiveCounter().subscribe(event -> { ObjectMessage msg = event.getMessage(); if (msg != null && msg.getOperation().getAction() == ObjectOperationAction.OBJECT_DELETE) { System.out.println("Old counter was deleted"); } }); } // Replace with a new counter - the old instance becomes unreachable LiveCounter replacementCounter = LiveCounter.create(0); rootObject.set("visits", LiveMapValue.of(replacementCounter)).join(); // The subscription will fire with the delete notification ``` ## Metadata Objects include metadata that helps with synchronization, conflict resolution and managing the object lifecycle. ### Object IDs Every object has a unique identifier that distinguishes it from all other objects. You can access an object's ID using the [Instance](https://ably.com/docs/liveobjects/concepts/instance.md) API: #### Javascript ``` const counterInstance = rootObject.get('visits').instance(); if (counterInstance) { const objectId = counterInstance.id; console.log('Object ID:', objectId); // e.g., "counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000" } ``` #### Swift ``` if case .liveCounter(let counterInstance)? = try rootObject.get(key: "visits").instance() { let objectId = counterInstance.id print("Object ID: \(objectId)") // e.g., "counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000" } ``` #### Java ``` Instance counterInstance = rootObject.get("visits").instance(); if (counterInstance != null) { String objectId = counterInstance.asLiveCounter().getId(); System.out.println("Object ID: " + objectId); // e.g., "counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000" } ``` Object IDs are opaque strings that uniquely identify each object instance. The client library automatically manages object IDs and allows you to work with object references directly. However, you may specify object IDs explicitly when using the [REST API](https://ably.com/docs/liveobjects/rest-api-usage.md). ### Tombstones Tombstones are markers indicating an object or map entry has been deleted. * A tombstone is created for an object when it becomes [unreachable](#reachability) from the channel object. * A tombstone is created for a map entry when it is [removed](https://ably.com/docs/liveobjects/map.md#remove) Tombstones protect against lagging clients from re-introducing a deleted value, ensuring all clients eventually converge on the same state. They are eventually garbage collected after a safe period of time. ### Timeserials When an operation message is published it is assigned a unique logical timestamp called a "timeserial". This timeserial is stored on map entries in order to implement last-write-wins conflict resolution semantics. Additionally, all objects store the timeserial of the last operation that was applied to the object. Since Ably operates fully independent data centers, these timeserials are stored on a per-site basis. Timeserial metadata is used for internal purposes and is not directly exposed in client libraries. However, it can be viewed using the [REST API](https://ably.com/docs/liveobjects/rest-api-usage.md). ## Related Topics - [PathObject](https://ably.com/docs/liveobjects/concepts/path-object.md): Learn about PathObject, a path-based API for accessing and manipulating LiveObjects data structures - [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.