# LiveMap `LiveMap` is a synchronized key/value data structure that stores [primitive values](https://ably.com/docs/liveobjects/concepts/objects.md#primitive-types), such as numbers, strings, booleans, binary data, JSON-serializable objects or arrays and other live [object types](https://ably.com/docs/liveobjects/concepts/objects.md#object-types). It ensures that all updates are correctly applied and synchronized across clients in realtime, automatically resolving conflicts with last-write-wins (LWW) semantics. You interact with `LiveMap` through a [PathObject](https://ably.com/docs/liveobjects/concepts/path-object.md) or by obtaining a specific [Instance](https://ably.com/docs/liveobjects/concepts/instance.md). ## Create a map Create a `LiveMap` using the `LiveMap.create()` static method and assign it to a path: ### Javascript ``` import { LiveMap } from 'ably/liveobjects'; // Create an empty map await rootObject.set('settings', LiveMap.create()); // Create a map with initial data await rootObject.set('user', LiveMap.create({ name: 'Alice', score: 42, active: true })); // Create nested maps await rootObject.set('config', LiveMap.create({ theme: LiveMap.create({ color: 'dark', fontSize: 14 }) })); ``` ### Java ``` // rootObject obtained from channel.object.get() // Create an empty map rootObject.set("settings", LiveMapValue.of(LiveMap.create())).join(); // Create a map with initial data Map userData = Map.of( "name", LiveMapValue.of("Alice"), "score", LiveMapValue.of(42), "active", LiveMapValue.of(true)); LiveMap user = LiveMap.create(userData); // Assign the user map to the channel object rootObject.set("user", LiveMapValue.of(user)).join(); // Create a map for the nested theme LiveMap theme = LiveMap.create(Map.of( "color", LiveMapValue.of("dark"), "fontSize", LiveMapValue.of(14))); // Create the config map, referencing the nested theme map LiveMap config = LiveMap.create(Map.of( "theme", LiveMapValue.of(theme))); // Assign the config map to the channel object rootObject.set("config", LiveMapValue.of(config)).join(); ``` Unlike JavaScript, Java requires every value passed to `set()` to be wrapped in `LiveMapValue.of(...)`, including the `LiveMap.create()` and `LiveCounter.create()` blueprints. `LiveMap.create()` returns a value type that describes the initial data for a new map. The actual object is created when assigned to a path. Each assignment creates a distinct object with its own unique ID: ### Javascript ``` const mapValue = LiveMap.create({ theme: 'dark' }); // Each assignment creates a different object await rootObject.set('map1', mapValue); await rootObject.set('map2', mapValue); // map1 and map2 are different objects with different IDs const id1 = rootObject.get('map1').instance()?.id; const id2 = rootObject.get('map2').instance()?.id; console.log(id1 === id2); // false ``` ### Java ``` LiveMap mapValue = LiveMap.create(Map.of("theme", LiveMapValue.of("dark"))); // Each assignment creates a different object rootObject.set("map1", LiveMapValue.of(mapValue)).join(); rootObject.set("map2", LiveMapValue.of(mapValue)).join(); // map1 and map2 are different objects with different IDs Instance map1Instance = rootObject.get("map1").instance(); Instance map2Instance = rootObject.get("map2").instance(); if (map1Instance != null && map2Instance != null) { System.out.println(map1Instance.asLiveMap().getId().equals(map2Instance.asLiveMap().getId())); // false } ``` ## Get map values Access a `LiveMap` through a [PathObject](https://ably.com/docs/liveobjects/concepts/path-object.md) for path-based operations, or obtain a specific [Instance](https://ably.com/docs/liveobjects/concepts/instance.md) to work with the underlying object directly. Use the `get()` method to navigate to entries within the map.Cast with `asLiveMap()`, then use the `get()` method to navigate to entries within the map. What you can do with the result depends on the type of value stored in the entry: - For entries containing [primitive values](https://ably.com/docs/liveobjects/concepts/objects.md#primitive-types) or [`LiveCounter`](https://ably.com/docs/liveobjects/counter.md) objects, use the `value()` method to get the current value. - For entries containing nested `LiveMap` objects, use `get()` to continue navigating deeper into the structure. - For entries containing [primitive values](https://ably.com/docs/liveobjects/concepts/objects.md#primitive-types) or [`LiveCounter`](https://ably.com/docs/liveobjects/counter.md) objects, infer the matching type and read its value (`asString().value()`, `asLiveCounter().value()`, and so on). - For entries containing nested `LiveMap` objects, cast with `asLiveMap()` to continue navigating deeper with `get()`. ### Javascript ``` const rootObject = await channel.object.get(); // PathObject access: path-based operations that resolve at runtime const theme = rootObject.get('settings').get('theme'); // settings holds a LiveMap with a 'theme' key console.log(theme.value()); // e.g. 'dark' const visits = rootObject.get('visits'); // visits holds a LiveCounter console.log(visits.value()); // e.g. 5 // Instance access: reference to a specific map object const settingsInstance = rootObject.get('settings').instance(); console.log(settingsInstance?.get('theme')?.value()); // e.g. 'dark' (primitive string) ``` ### Java ``` LiveMapPathObject rootObject = channel.object.get().join(); // PathObject access: path-based operations that resolve at runtime PathObject theme = rootObject.get("settings").asLiveMap().get("theme"); System.out.println(theme.asString().value()); // e.g. "dark" System.out.println(rootObject.get("visits").asLiveCounter().value()); // e.g. 5.0 // Instance access: reference to a specific map object Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { Instance themeInstance = settingsInstance.asLiveMap().get("theme"); if (themeInstance != null) { System.out.println(themeInstance.asString().value()); // e.g. "dark" (primitive string) } } ``` ## Get compact object Get a JavaScript object representation of the map using the `compact()` or `compactJson()` methods: ### Javascript ``` // Get a PathObject to a LiveMap stored in 'settings' const settings = rootObject.get('settings'); console.log(settings.compact()); // e.g. { theme: 'dark', fontSize: 14, notifications: true } // Get the Instance of a LiveMap stored in 'settings' const settingsInstance = rootObject.get('settings').instance(); console.log(settingsInstance?.compact()); // e.g. { theme: 'dark', fontSize: 14, notifications: true } ``` Get a JSON representation of the map using the `compactJson()` method (the Java SDK has no `compact()` equivalent): ### Java ``` // Get a PathObject to a LiveMap stored in 'settings' System.out.println(rootObject.get("settings").compactJson()); // JsonElement, null if unresolved // e.g. {"theme":"dark","fontSize":14.0,"notifications":true} // Get the Instance of a LiveMap stored in 'settings' Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { System.out.println(settingsInstance.asLiveMap().compactJson()); // non-null JsonObject } ``` ## Set map values Set a value for a key in the map using the `set()` method. You can store primitive values or other live [object types](https://ably.com/docs/liveobjects/concepts/objects.md#object-types): ### Javascript ``` // PathObject: set values at path await rootObject.get('settings').set('theme', 'dark'); await rootObject.get('settings').set('fontSize', 14); await rootObject.get('settings').set('notifications', true); // Set a nested map await rootObject.get('settings').set('advanced', LiveMap.create({ debugMode: false, logLevel: 'info' })); // Set a counter await rootObject.get('settings').set('visits', LiveCounter.create(0)); // Instance: set values on specific map instance const settingsInstance = rootObject.get('settings').instance(); await settingsInstance?.set('theme', 'dark'); await settingsInstance?.set('fontSize', 14); ``` ### Java ``` // PathObject: set values at path LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); settings.set("theme", LiveMapValue.of("dark")).join(); settings.set("fontSize", LiveMapValue.of(14)).join(); settings.set("notifications", LiveMapValue.of(true)).join(); // Set a nested map LiveMap advanced = LiveMap.create(Map.of( "debugMode", LiveMapValue.of(false), "logLevel", LiveMapValue.of("info"))); settings.set("advanced", LiveMapValue.of(advanced)).join(); // Set a counter LiveCounter visitsCounter = LiveCounter.create(0); settings.set("visits", LiveMapValue.of(visitsCounter)).join(); // Instance: set values on specific map instance Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { settingsInstance.asLiveMap().set("theme", LiveMapValue.of("dark")).join(); settingsInstance.asLiveMap().set("fontSize", LiveMapValue.of(14)).join(); } ``` ## Remove a key Remove a key from the map using the `remove()` method: ### Javascript ``` // PathObject: remove key at path await rootObject.get('settings').remove('oldSetting'); // Instance: remove key on specific map instance const settingsInstance = rootObject.get('settings').instance(); await settingsInstance?.remove('oldSetting'); ``` ### Java ``` // PathObject: remove key at path rootObject.get("settings").asLiveMap().remove("oldSetting").join(); // Instance: remove key on specific map instance Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { settingsInstance.asLiveMap().remove("oldSetting").join(); } ``` ## Get map size Get the number of keys in the map using the `size()` method: ### Javascript ``` // PathObject: get size at path const settings = rootObject.get('settings'); console.log(settings.size()); // e.g. 5 // Instance: get size of specific map instance const settingsInstance = rootObject.get('settings').instance(); console.log(settingsInstance?.size()); // e.g. 5 ``` ### Java ``` // PathObject: get size at path - Long, or null when not a LiveMap Long size = rootObject.get("settings").asLiveMap().size(); System.out.println(size); // e.g. 5 // Instance: get size of specific map instance - non-null Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { System.out.println(settingsInstance.asLiveMap().size()); // e.g. 5 } ``` ## Enumerate entries Iterate over the map's keys, values, or entries. When iterating using a `PathObject`, values are returned as a `PathObject` for the nested path. When iterating using an `Instance`, values are returned as an `Instance` for the entry: ### Javascript ``` // PathObject: iterate with PathObject values const settings = rootObject.get('settings'); for (const [key, value] of settings.entries()) { console.log(`${key}:`, value.value()); } for (const key of settings.keys()) { console.log('Key:', key); } for (const value of settings.values()) { console.log('Value:', value.value()); } // Instance: iterate with Instance values const settingsInstance = rootObject.get('settings').instance(); if (settingsInstance) { for (const [key, valueInstance] of settingsInstance.entries()) { console.log(`${key}:`, valueInstance.value()); } for (const key of settingsInstance.keys()) { console.log('Key:', key); } for (const value of settingsInstance.values()) { console.log('Value:', value.value()); } } ``` ### Java ``` // PathObject: iterate with PathObject values LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); for (Map.Entry entry : settings.entries()) { System.out.println(entry.getKey() + ": " + entry.getValue().compactJson()); } for (String key : settings.keys()) { System.out.println("Key: " + key); } for (PathObject value : settings.values()) { System.out.println("Value: " + value.compactJson()); } // Instance: iterate with Instance values Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { for (Map.Entry entry : settingsInstance.asLiveMap().entries()) { System.out.println(entry.getKey() + ": " + entry.getValue().compactJson()); } for (String key : settingsInstance.asLiveMap().keys()) { System.out.println("Key: " + key); } for (Instance value : settingsInstance.asLiveMap().values()) { System.out.println("Value: " + value.compactJson()); } } ``` ## Batch multiple operations Group multiple map operations into a single atomic message using the `batch()` method. All operations within the batch are sent as one logical unit which succeed or fail together: ### Javascript ``` // PathObject: batch operations on map at path await rootObject.get('settings').batch((ctx) => { ctx.set('theme', 'dark'); ctx.set('fontSize', 14); ctx.set('notifications', true); ctx.remove('oldSetting'); }); // Instance: batch operations on specific map instance const settingsInstance = rootObject.get('settings').instance(); await settingsInstance?.batch((ctx) => { ctx.set('theme', 'dark'); ctx.set('fontSize', 14); }); ``` ## Subscribe to updates Subscribe to `LiveMap` updates to receive realtime notifications when the map changes. `PathObject` subscriptions observe a location and automatically track changes even if the `LiveMap` instance at that path is replaced. `Instance` subscriptions track a specific `LiveMap` instance, following it even if it moves in the channel object. ### Javascript ``` // PathObject: observe location - tracks changes even if map instance is replaced const settings = rootObject.get('settings'); const { unsubscribe } = settings.subscribe(() => { console.log('Settings:', settings.compact()); }); // Later, stop listening to changes unsubscribe(); // Instance: track specific map instance - follows it even if moved in object tree const settingsInstance = rootObject.get('settings').instance(); if (settingsInstance) { const { unsubscribe } = settingsInstance.subscribe(() => { console.log('Settings:', settingsInstance.compact()); }); // Later, stop listening to changes unsubscribe(); } ``` ### Java ``` // PathObject: observe location - tracks changes even if map instance is replaced LiveMapPathObject settings = rootObject.get("settings").asLiveMap(); Subscription subscription = settings.subscribe(event -> System.out.println("Settings: " + settings.compactJson())); // Later, stop listening to changes subscription.unsubscribe(); // Instance: track specific map instance - follows it even if moved in object tree Instance settingsInstance = rootObject.get("settings").instance(); if (settingsInstance != null) { Subscription instanceSub = settingsInstance.asLiveMap().subscribe(event -> System.out.println("This settings instance changed")); // Later, stop listening to changes instanceSub.unsubscribe(); } ``` Alternatively, use the `subscribeIterator()` method for an async iterator syntax: ### Javascript ``` // PathObject: observe location - tracks changes even if map instance is replaced const settings = rootObject.get('settings'); for await (const _ of settings.subscribeIterator()) { console.log('Settings:', settings.compact()); if (someCondition) { break; // Unsubscribes } } // Instance: track specific map instance - follows it even if moved in object tree const settingsInstance = rootObject.get('settings').instance(); if (settingsInstance) { for await (const _ of settingsInstance.subscribeIterator()) { console.log('Settings:', settingsInstance.compact()); if (someCondition) { break; // Unsubscribes } } } ``` The Java SDK has no `subscribeIterator()` equivalent; a listener plus the returned `Subscription` is the only subscription form. ## Create LiveMap A `LiveMap` instance can be created using the `channel.objects.createMap()` method. It must be stored inside another `LiveMap` object that is reachable from the [root object](https://ably.com/docs/liveobjects/concepts/objects.md#root-object). `channel.objects.createMap()` is asynchronous, as the client sends the create operation to the Ably system and waits for an acknowledgment of the successful map creation. ### Swift ``` let map = try await channel.objects.createMap() try await root.set(key: "myMap", value: .liveMap(map)) ``` Optionally, you can specify an initial key/value structure when creating the map: ### Swift ``` // Pass a Dictionary reflecting the initial state let map = try await channel.objects.createMap(entries: ["foo": "bar", "baz": 42]) // You can also pass other objects as values for keys try await channel.objects.createMap(entries: ["nestedMap": .liveMap(map)]) ``` ## Get value for a key Get the current value for a key in a map using the `LiveMap.get()` method: ### Swift ``` let value = try map.get(key: "my-key") print("Value for my-key: \(String(describing: value))") ``` ## Subscribe to data updates You can subscribe to data updates on a map to receive realtime changes made by you or other clients. Subscribe to data updates on a map using the `LiveMap.subscribe()` method: ### Swift ``` try map.subscribe { update, _ in do { print("Map updated: \(try map.entries)") } catch { // Error handling of map.entries omitted for brevity } print("Update details: \(update)") } ``` The update object provides details about the change, listing the keys that were changed and indicating whether they were updated (value changed) or removed from the map. It may also include the client ID of the client that made the change, if the change can be attributed to a specific client. For example, the client ID may be missing if the update was triggered by data resynchronization after a disconnection and the change occurred while the client was offline. Example structure of an update object when the key `foo` is updated and the key `bar` is removed, made by a client with the ID `my-client`: ### Json ``` { "update": { "foo": "updated", "bar": "removed" }, "clientId": "my-client" } ``` ### Unsubscribe from data updates Use the `unsubscribe()` function returned in the `subscribe()` response to remove a map update listener: #### Swift ``` // Initial subscription let subscriptionResponse = try map.subscribe { _, _ in print("Map updated") } // To remove the listener subscriptionResponse.unsubscribe() ``` To remove a map update listener from _inside_ the listener function, you can call `unsubscribe()` on the subscription response that is passed as the second argument to the listener function: #### Swift ``` try map.subscribe { _, subscriptionResponse in // Remove the listener so that this callback // no longer gets called subscriptionResponse.unsubscribe() } ``` Use the `LiveMap.unsubscribeAll()` method to deregister all map update listeners: #### Swift ``` map.unsubscribeAll(); ``` ## Set keys in a LiveMap Set a value for a key in a map by calling `LiveMap.set(key:value:)`. This operation is synchronized across all clients and triggers data subscription callbacks for the map, including on the client making the request. Keys in a map can contain numbers, strings, booleans, `Data`, JSON-serializable objects or arrays and other `LiveMap` and `LiveCounter` objects. This operation is asynchronous, as the client sends the corresponding update operation to the Ably system and waits for acknowledgment of the successful map key update. ### Swift ``` try await map.set(key: "foo", value: "bar") try await map.set(key: "baz", value: 42) // Can also set a reference to another object let counter = try await channel.objects.createCounter() try await map.set(key: "counter", value: .liveCounter(counter)) ``` ## Remove a key from a LiveMap Remove a key from a map by calling `LiveMap.remove(key:)`. This operation is synchronized across all clients and triggers data subscription callbacks for the map, including on the client making the request. This operation is asynchronous, as the client sends the corresponding remove operation to the Ably system and waits for acknowledgment of the successful map key removal. ### Swift ``` try await map.remove(key: "foo") ``` ## Get the number of entries in a LiveMap Get the number of entries in a map using `LiveMap.size`: ### Swift ``` let map = try await channel.objects.createMap(entries: ["foo": "bar", "baz": "qux"]) print(try map.size) // e.g. 2 ``` ## Iterate over key/value pairs Iterate over key/value pairs, keys or values using the `LiveMap.entries`, `LiveMap.keys` and `LiveMap.values` properties respectively. These properties are all `Array`-valued. Note that they do not guarantee that entries are returned in insertion order. ### Swift ``` for (key, value) in try map.entries { print("Key: \(key), Value: \(value)") } for key in try map.keys { print("Key: \(key)") } for value in try map.values { print("Value: \(value)") } ``` ## Subscribe to lifecycle events Subscribe to lifecycle events on a map using the `LiveMap.on(event:callback:)` method: ### Swift ``` map.on(event: .deleted) { _ in print("Map has been deleted") } ``` Read more about [objects lifecycle events](https://ably.com/docs/liveobjects/lifecycle.md#objects). ### Unsubscribe from lifecycle events Use the `off()` function returned in the `on()` response to remove a lifecycle event listener: #### Swift ``` // Initial subscription let eventResponse = map.on(event: .deleted) { _ in print("Map deleted") } // To remove the listener eventResponse.off() ``` Use the `LiveMap.offAll()` method to deregister all lifecycle event listeners: #### Swift ``` map.offAll() ``` ## Create nested structures A `LiveMap` can store other `LiveMap` or `LiveCounter` objects as values for its keys, enabling you to build complex, hierarchical object structure. This enables you to represent complex data models in your applications while ensuring realtime synchronization across clients. ### Swift ``` // Create a hierarchy of objects using LiveMap let counter = try await channel.objects.createCounter() let map = try await channel.objects.createMap(entries: ["nestedCounter": .liveCounter(counter)]) let outerMap = try await channel.objects.createMap(entries: ["nestedMap": .liveMap(map)]) try await root.set(key: "outerMap", value: .liveMap(outerMap)) // resulting structure: // root (LiveMap) // └── outerMap (LiveMap) // └── nestedMap (LiveMap) // └── nestedCounter (LiveCounter) ``` ## Related Topics - [LiveCounter](https://ably.com/docs/liveobjects/counter.md): Create, update and receive updates for a numerical counter that synchronizes state across clients in realtime. ## 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.