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:

let subscription = try await adk.subscribe(channel: "YOUR_SHARED_OBJECT_CHANNEL")

if let live = subscription as? LiveObjSubscription {
// 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
let sharedState = live.state()
// Now you can work with it like a regular swift object
sharedState["document"]["title"].set("New Document Title")
await live.flush()

Delete a key​

// Safe delete (no error if the key doesn't exist)
sharedState["document"]["title"].delete()
await live.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​

let count = sharedState["items"].push("First item")
await live.flush()

Pop​

let removed = sharedState["items"].pop()
await live.flush()

Remove at index​

let removed = sharedState["items"].removeAt(2)
await live.flush()

Splice (replace a range)​

let removed = sharedState["items"].splice(
start: 1,
deleteCount: 1,
insert: ["new item"]
)

await live.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
sharedState["title"].set("New Title")
sharedState["content"].set("Updated content")
// Manually trigger synchronization —
// all pending changes are batched and sent as a single operation
await live.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
let query = live.query(path: "user")
// Get current value without listening continuously
let initial = await query.execute()
print("Initial:", initial)
// `initial` holds the current value at the path 'user'

Listen for changes​

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

let dispose = await query.listen { data in

// Handle nil (path cleared or doesn't exist)
guard let data = data else {
return
}

// Ensure expected type
guard let map = data as? [String: Any] else {
// Unexpected shape — handle defensively
return
}

// Iterate over entries
for (key, value) in map {
let username = key
let userData = value

// Process user data
print("User:", username, "Data:", userData)
}
}