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)
}
}