Skip to main content

Shared Object Channels

Shared Object Channels in ART provide real-time collaborative editing capabilities using CRDT (Conflict-free Replicated Data Types) technology. These channels enable multiple users to simultaneously edit the same data structure with automatic conflict resolution and real-time synchronization.

Unlike regular messaging channels that send discrete messages, shared object channels maintain a synchronized data structure that multiple clients can edit simultaneously. Think of it like Google Docs for any object — changes made by one user are instantly reflected for all other users.

Key Features

  • CRDT-Based Synchronization: Uses advanced conflict-free replicated data types
  • Real-time Updates: Changes propagate instantly to all connected clients
  • Conflict Resolution: Automatic handling of concurrent edits without data loss
  • RGA Arrays: Replicated Growable Arrays for ordered list operations
  • Echo Suppression: Prevents infinite loops from your own changes

How Shared Object Channels Work

Shared Object Channels operate through a sophisticated CRDT (Conflict-free Replicated Data Types) system that maintains synchronized state across multiple clients. When you subscribe to a shared object channel, ART creates a live data structure that automatically synchronizes changes between all connected users in real-time.

Subscribing to Shared Object Channels

Subscribing to a shared object channel uses the same adk.subscribe call as any other channel:

LiveObjSubscription = await _adk.Subscribe("YOUR CHANNEL NAME") as SharedObjectChannel;
if (LiveObjSubscription != null)
{
// CRDT-enabled subscription
}
else
{
// Default or encrypted channel — no CRDT state
}

The system initializes with any existing state from the server and creates a unique replica ID for your client to handle conflict resolution.

Accessing the Shared State

The core of shared object channels is the state() method, which returns a CRDT Proxy object that behaves like a nested Map, with .set() / .delete() / array helpers that are automatically synchronised:

Set a value

// Get the shared state object
var state = LiveObjSubscription.State()
// Now you can work with it like a regular unity object
state["document"]["title"].Set(title);
await LiveObjSubscription.Flush();

Delete a key

// Safe delete (no error if the key doesn't exist)
state["document"]["title"].delete()
await LiveObjSubscription.flush()

Every set() / delete() is intercepted by the CRDT engine and converted into operations that are synchronized across all connected clients.

Working with Arrays

Shared object channels provide full array support using RGA (Replicated Growable Array) semantics, which handle concurrent insertions and deletions correctly:

Push

var push = state["items"].Push("First item")
await LiveObjSubscription.flush()

Pop

var removed = state["items"].Pop();
await LiveObjSubscription.Flush();

Remove at index

var removeIndex = state["items"].Unshift(item);
await LiveObjSubscription.Flush();

Splice (replace a range)

var splice = state["items"].splice(
start: 1,
deleteCount: 1,
insert: ["new item"]
)

await LiveObjSubscription.flush()

The RGA implementation ensures that concurrent array operations from different users are resolved consistently, maintaining the intended order even when users edit simultaneously.

Manual Synchronization Control

Changes are automatically synchronized, but you can control when pending operations are sent to the server using the flush() method:

// Make multiple changes
state["title"].set("New Title")
state["content"].set("Updated content")
// Manually trigger synchronization —
// all pending changes are batched and sent as a single operation
await LiveObjSubscription.flush()

By default, the Swift ADK batches operations with a 50ms trailing delay to optimize network usage; flush() allows immediate synchronization when needed.

Listening to Updates

The query() method allows you to observe specific paths of the shared object — useful for optimising updates in large data structures:

Get the current value

// Query specific path in the object
var (execute, listen) = LiveObjSubscription.Query("user");

Listen for changes

query.listen() fires immediately with the current value, then again on every update at (or under) that path:

listen(data =>
{
Log("ART", "Changes : $data")
});