Batch operations

Open in

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:

JavaScript

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:

JavaScript

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:

JavaScript

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:

JavaScript

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 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:

JavaScript

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:

JavaScript

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.

JavaScript

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:

JavaScript

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:

JavaScript

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:

JavaScript

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:

JavaScript

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