# Operations LiveObjects operations define how object data is updated and synchronized across multiple clients. When you create or update an object, the change is expressed as an _operation_ that is sent as an [object message](https://ably.com/docs/metadata-stats/stats#messages) on the channel. The operation is then applied to the object instance on all clients that are subscribed to the channel. This document explains the key concepts you need to know when working with operations. ## Operation types Each object type supports specific operations that modify the object's data. ### LiveMap operations [LiveMap](https://ably.com/docs/liveobjects/map) supports the following operations: * `set`: Set a value for a key * `remove`: Remove a key and its value The value of an entry in a `LiveMap` instance can be a [primitive type](https://ably.com/docs/liveobjects/concepts/objects#primitive-types) or a [reference](https://ably.com/docs/liveobjects/concepts/objects#composability) to another object. ```javascript await myObject.get('user').set('username', 'alice'); await myObject.get('user').remove('status'); ``` ```swift // Set a value for a key try await map.set(key: "username", value: "alice") // Remove a key try await map.remove(key: "username") ``` ```java // Set a value for a key map.set("username", LiveMapValue.of("alice")); // Remove a key map.remove("username"); ``` ### LiveCounter operations [LiveCounter](https://ably.com/docs/liveobjects/counter) supports the following operations: * `increment`: Increment the counter by a specified amount * `decrement`: Decrement the counter by a specified amount The amount is a double-precision floating-point number, which is the same as underlying type of a [LiveCounter](https://ably.com/docs/liveobjects/concepts/objects#livecounter) value. ```javascript await myObject.get('visits').increment(5); await myObject.get('score').decrement(2); ``` ```swift // Increment counter by 5 try await counter.increment(amount: 5) // Decrement counter by 2 try await counter.decrement(amount: 2) ``` ```java // Increment counter by 5 counter.increment(5); // Decrement counter by 2 counter.decrement(2); ``` ### Create operations Create operations are used to instantiate new objects of a given type. Use `LiveMap.create()` and `LiveCounter.create()` to create new objects. These methods create special value types that can be assigned directly to paths: ```javascript // Create and assign a map with initial values await myObject.set('user', LiveMap.create({ username: 'alice', status: 'online' })); // Create and assign a counter with initial value await myObject.set('score', LiveCounter.create(100)); // Create nested structures await myObject.set('profile', LiveMap.create({ name: 'Alice', score: LiveCounter.create(0), settings: LiveMap.create({ theme: 'dark', notifications: true }) })); ``` When a create operation is processed, an [object ID](https://ably.com/docs/liveobjects/concepts/objects#object-ids) is automatically generated for the new object instance. A create operation can optionally specify an initial value for the object. ```swift // Create a map with initial values let userMap = try await channel.objects.createMap(entries: [ "username": "alice", "status": "online", ]) // Create a counter with initial value let scoreCounter = try await channel.objects.createCounter(count: 100) ``` ```java // Create a map with initial values Map entries = Map.of( "username", LiveMapValue.of("alice"), "status", LiveMapValue.of("online") ); LiveMap userMap = channel.getObjects().createMap(entries); // Create a counter with initial value LiveCounter scoreCounter = channel.getObjects().createCounter(100); ``` When a create operation is processed, an [object ID](https://ably.com/docs/liveobjects/concepts/objects#object-ids) for the new object instance is automatically generated for the object. ## Object IDs Every operation is expressed relative to a specific object instance, identified by its [object ID](https://ably.com/docs/liveobjects/concepts/objects#object-ids), which determines which object the operation is applied to. When using a `PathObject`, the specific object instance at the given path is evaluated at the time a method is called that updates the object. The client library will then publish an operation targeting the resolved object. When using a client library object IDs are handled automatically, allowing you work directly with object references: ```javascript // The published operation targets the object ID of the object at 'user' await myObject.get('user').set('username', 'alice'); // Get the specific instance and its ID const userInstance = myObject.get('user').instance(); if (userInstance) { console.log('Object ID:', userInstance.id()); await userInstance.set('username', 'alice'); } ``` ```swift // The published operation targets the object ID of the `userMap` object instance try await userMap.set(key: "username", "alice") ``` ```java // The published operation targets the object ID of the `userMap` object instance userMap.set("username", LiveMapValue.of("alice")); ``` Therefore it is important that you obtain an up-to-date object instance before performing operations on an object. For example, you can [subscribe](https://ably.com/docs/liveobjects/map#subscribe-data) to a `LiveMap` instance to ensure you always have an up-to-date reference to any child objects in the map: { /* We can't map the JS example directly because Swift concurrency prevents us from mutating local variables in the way that the JS example does, so I tried to show how we might need to handle this scenario in a real-world app where things are isolated to the main actor. But it's long and ugly. */ } ```swift struct MyView: View { var root: any LiveMap @State private var myCounter: (any LiveCounter)? var body: some View { Button("Increment the counter") { Task { try await myCounter?.increment(amount: 1) } }.onAppear { do { myCounter = try root.get(key: "myCounter")?.liveCounterValue // We keep a reference to the latest value that the root map // stores at the "myCounter" key, to ensure that upon tapping // the button, we increment the correct counter. try root.subscribe { _, _ in MainActor.assumeIsolated { do { myCounter = try root.get(key: "myCounter")?.liveCounterValue } catch { // Error handling of root.get(key:) omitted for brevity } } } } catch { // Error handling of root.get(key:) omitted for brevity } } } } ``` ```java LiveMap root = channel.getObjects().getRoot(); // Keep a reference to the counter object LiveCounter myCounter = root.get("myCounter").asLiveCounter(); // Subscribe to root changes to keep counter reference up-to-date root.subscribe((mapUpdate) -> { if (mapUpdate.getUpdate().containsKey("myCounter")) { myCounter = root.get("myCounter").asLiveCounter(); } }); // Before incrementing, ensure we have an up-to-date object reference if // the counter instance at the 'myCounter' key in the root map changes myCounter.increment(1); ``` In the [REST API](https://ably.com/docs/liveobjects/rest-api-usage#updating-objects-by-id), the relationship between operations and object IDs is made explicit: ```shell curl -X POST https://main.realtime.ably.net/channels/my-channel/objects \ -u "your-api-key" -H "Content-Type: application/json" \ --data \ '{ "operation": "MAP_SET", "objectId": "root", "data": {"key": "username", "value": {"string": "alice"}} }' ``` ## Batch operations [Batch operations](https://ably.com/docs/liveobjects/batch) can be used to batch a set of operations together: * Multiple operations are grouped into a single atomic unit * All operations in the batch either succeed together or fail together * Operations in a batch are sent as a single message * No operations from other clients can be interleaved within a batch ## Object message properties Each operation is carried by an `ObjectMessage`, which is surfaced in [subscriptions](https://ably.com/docs/liveobjects/concepts/path-object#subscribe) and provides metadata about the operation and who performed it. ### ObjectMessage The following are the properties of an `ObjectMessage`: | Property | Description | |----------|-------------| | **id** | Unique ID assigned by Ably to this object message | | **clientId** | The [ID of the client](https://ably.com/docs/auth/identified-clients) that published this operation | | **connectionId** | The ID of the connection used to publish this operation | | **timestamp** | The timestamp of when the object message was received by Ably, as milliseconds since the Unix epoch | | **channel** | The name of the channel the object message was published to | | **operation** | An [`ObjectOperation`](#object-operation) describing the operation that was applied | | **serial** | An opaque string that uniquely identifies this object message | | **serialTimestamp** | A timestamp derived from the `serial` field | | **siteCode** | An opaque string that uniquely identifies the Ably site the object message was published to | | **extras** | A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include `headers` | ### ObjectOperation The `operation` field of an `ObjectMessage` contains an `ObjectOperation` with the following properties: | Property | Description | |----------|-------------| | **action** | The operation action. One of: `'map.create'`, `'map.set'`, `'map.remove'`, `'counter.create'`, `'counter.inc'`, or `'object.delete'` | | **objectId** | The ID of the object the operation was applied to | | **mapOp** | Present for map mutation operations (`'map.set'`, `'map.remove'`). Contains `key` (the key that was modified) and optionally `data` (an `ObjectData` representing the value assigned to the key, present only for `'map.set'` operations) | | **counterOp** | Present for counter increment operations (`'counter.inc'`). Contains `amount` (the value added to the counter) | | **map** | Present for `'map.create'` operations. Defines the initial value of the map object with optional `semantics` (conflict-resolution strategy) and `entries` (initial key-value pairs) | | **counter** | Present for `'counter.create'` operations. Defines the initial value of the counter object with optional `count` (initial counter value) | #### ObjectData The `data` field in `mapOp` is an `ObjectData` object that represents the value assigned to a map key. It has the following properties: | Property | Description | |----------|-------------| | **objectId** | A reference to another object (such as a `LiveMap` or `LiveCounter`) by its object ID. Present when the value is a LiveObject | | **value** | A decoded primitive value (string, number, boolean, JSON-serializable object or array, or binary data). Present when the value is a primitive type |