Batch operations allow multiple updates to be grouped into a single channel message and applied atomically. It ensures that all operations in a batch either succeed together or are discarded entirely. Batching is essential when multiple related updates to channel objects must be applied as a single atomic unit, for example, when application logic depends on multiple objects being updated simultaneously. Batching ensures that all operations in the batch either succeed or fail together.
Create a batch context
To batch object operations together, use the batch() method on a PathObject or Instance. This method accepts a callback function that receives a batch context that allows you to construct operations. Ably publishes these operations together as a single channel message. If an error occurs publishing the batched operations, all operations are discarded, preventing partial updates and ensuring atomicity.
Call batch() on a PathObject to group operations on that path:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const myObject = await channel.object.get();
// Batch multiple operations on the channel object
await myObject.batch((ctx) => {
ctx.set('foo', 'bar');
ctx.set('baz', 42);
ctx.remove('oldKey');
});
// Batch operations on a nested path
await myObject.get('settings').batch((ctx) => {
ctx.set('theme', 'dark');
ctx.set('fontSize', 14);
ctx.set('notifications', true);
});
// Batch operations on a counter
await myObject.get('visits').batch((ctx) => {
ctx.increment(5);
ctx.increment(3);
ctx.decrement(2);
});You can only call batch() on an PathObject whose path resolves to a LiveMap or LiveCounter instance:
1
2
3
4
5
6
7
8
const myObject = await channel.object.get();
try {
// Call batch() on 'username', which resolves to a string value
await myObject.get('username').batch((ctx) => {});
} catch (err) {
// Error 92007: Cannot batch operations on a non-LiveObject at path: username
}You can also call batch() on an Instance to batch operations on that specific object:
1
2
3
4
5
6
7
8
9
const settingsInstance = myObject.get('settings').instance();
if (settingsInstance) {
await settingsInstance.batch((ctx) => {
ctx.set('theme', 'dark');
ctx.set('fontSize', 14);
ctx.remove('oldSetting');
});
}Create objects in a batch
You can create new objects inside a batch using LiveMap.create() and LiveCounter.create(). This allows you to atomically create and assign multiple objects in a single operation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myObject = await channel.object.get();
await myObject.batch((ctx) => {
// Create and assign multiple objects atomically
ctx.set('user', LiveMap.create({
name: 'Alice',
score: LiveCounter.create(0),
settings: LiveMap.create({
theme: 'dark',
notifications: true
})
}));
ctx.set('visits', LiveCounter.create(0));
ctx.set('reactions', LiveMap.create({
likes: 0,
hearts: 0
}));
});Creating objects inside a batch ensures that all objects are created and assigned as a single atomic operation, preventing inconsistent intermediate states.
Navigate a batch context
Navigate to nested objects using the get() method on the batch context. Navigating a batch context with get() has the same behaviour as navigating an Instance:
1
2
3
4
5
6
7
8
await myObject.batch((ctx) => {
// Navigate to nested paths and perform operations
ctx.get('settings')?.set('theme', 'dark');
ctx.get('settings')?.get('preferences')?.set('language', 'en');
// Increment a nested LiveCounter instance
ctx.get('visits')?.increment(5);
});Cancel a batch
To explicitly cancel a batch before it is applied, throw an error inside the batch function. This prevents any queued operations from being applied:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
await myObject.batch((ctx) => {
// Cancel the entire batch if a required value is missing
const value = ctx.get('visits')?.value();
if (value === undefined) {
throw new Error('visits key is missing');
}
// Will not be applied if visits is missing
ctx.set('lastSeen', Date.now());
});
} catch (err) {
// Batch operations not applied if visits is missing
}Understand batch context behavior
The batch context has the same API as Instance, except for batch() itself, but all mutation methods are synchronous and queue operations instead of sending them immediately. After the callback completes, all queued operations are sent together in a single channel message.
1
2
3
4
5
6
7
8
9
10
try {
await myObject.batch((ctx) => {
// These operations are synchronous and queued
ctx.get('settings')?.set('theme', 'dark');
ctx.get('visits')?.increment();
});
// All operations published successfully
} catch (err) {
// Failed to publish operations, none of them were published
}Since the batch callback is synchronous, you can read current values inside a batch context without intermediate updates from other clients being applied between reads:
1
2
3
4
5
await myObject.batch((ctx) => {
ctx.get('settings')?.get('theme')?.value(); // "dark"
// no updates will be applied during execution of the batch callback
ctx.get('settings')?.get('theme')?.value(); // "dark"
});Operations on a batch context are not applied until they are all published, so you cannot read back data written inside the batch context callback:
1
2
3
4
5
6
7
8
9
10
11
await myObject.batch((ctx) => {
// Get the value at the time batch() was called
ctx.get('settings')?.get('theme')?.value(); // "dark"
// Update the value
ctx.get('settings')?.set('theme', 'light');
// The previous update will not be applied until the batch
// callback completes, so the new value cannot be read back
ctx.get('settings')?.get('theme')?.value(); // "dark"
});The batch context object cannot be used outside the callback function. Attempting to do so results in an error:
1
2
3
4
5
6
7
8
9
10
11
12
let context;
await myObject.batch((ctx) => {
context = ctx;
ctx.set('foo', 'bar');
});
// Calling any methods outside the batch callback will throw an error
try {
context.set('baz', 42);
} catch (error) {
// Error: Batch is closed
}When a batch operation is applied, the subscription is notified synchronously and sequentially for each operation included in the batch:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Subscribe to the channel object
myObject.subscribe(({ object, message }) => {
console.log("Update:", message?.operation.action, message?.operation?.mapOp.key);
});
// Perform a batch operation
await myObject.batch((ctx) => {
ctx.set('foo', 'bar');
ctx.set('baz', 42);
ctx.set('qux', 'hello');
});
// Update: map.set foo
// Update: map.set baz
// Update: map.set qux