(Note this issue was written by Claude at my request as part of a debugging session.)
Problem
MutableState was originally intended as a passive store of values with focused mutation helpers, but several methods on it have grown into behavioural orchestrators that perform multi-step operations: state transitions, applying sync data, draining publish-and-apply waiters, and invoking user callbacks.
The methods in question are:
MutableState.nosync_onChannelAttached
MutableState.nosync_handleObjectSyncProtocolMessage
MutableState.nosync_onChannelStateChanged
MutableState.nosync_drainPublishAndApplySyncWaiters
Because these live on MutableState, they are always called from within the mutableStateMutex scope (via withoutSync), so they have no ability to release the mutex partway through. This is problematic when their work includes invoking user callbacks that may themselves need to acquire the mutex — causing exclusive access violations.
If these methods lived on InternalDefaultRealtimeObjects instead, they could take and release the mutex as many times as needed with fine-grained control:
withoutSync { /* state transitions */ }
withoutSync { /* drain waiters, apply synthetic messages, collect callbacks */ }
- Call user callbacks — no mutex held
No completion threading, no async dispatch, no signalling fields — just straightforward sequential code that acquires the mutex when it needs it and releases it when it doesn't.
Current workaround
Rather than undertaking the larger refactor of moving these methods, we've worked around the immediate problem by:
- Changing the publish-and-apply waiter closures to receive
inout MutableState, so they can apply synthetic messages and pass mutable state access through to their callback without re-acquiring the mutex.
- Adding a private
nosync_publishAndApply(objectMessages:coreSDK:mutableStateCallback:) overload whose callback receives inout MutableState, used by createCounter/createMap to look up newly-created objects without re-acquiring the mutex.
This avoids the exclusive access violation but means the callback runs inside the mutex scope, relying on the callers (which are all async functions using CheckedContinuation) to naturally defer further work past the mutex boundary.
Proposed fix
Move the behavioural methods listed above from MutableState to InternalDefaultRealtimeObjects, restoring MutableState to its intended role as a data store with focused mutation helpers. This would eliminate the need for the workaround and make the mutex usage straightforward.
┆Issue is synchronized with this Jira Task by Unito
(Note this issue was written by Claude at my request as part of a debugging session.)
Problem
MutableStatewas originally intended as a passive store of values with focused mutation helpers, but several methods on it have grown into behavioural orchestrators that perform multi-step operations: state transitions, applying sync data, draining publish-and-apply waiters, and invoking user callbacks.The methods in question are:
MutableState.nosync_onChannelAttachedMutableState.nosync_handleObjectSyncProtocolMessageMutableState.nosync_onChannelStateChangedMutableState.nosync_drainPublishAndApplySyncWaitersBecause these live on
MutableState, they are always called from within themutableStateMutexscope (viawithoutSync), so they have no ability to release the mutex partway through. This is problematic when their work includes invoking user callbacks that may themselves need to acquire the mutex — causing exclusive access violations.If these methods lived on
InternalDefaultRealtimeObjectsinstead, they could take and release the mutex as many times as needed with fine-grained control:withoutSync { /* state transitions */ }withoutSync { /* drain waiters, apply synthetic messages, collect callbacks */ }No completion threading, no async dispatch, no signalling fields — just straightforward sequential code that acquires the mutex when it needs it and releases it when it doesn't.
Current workaround
Rather than undertaking the larger refactor of moving these methods, we've worked around the immediate problem by:
inout MutableState, so they can apply synthetic messages and pass mutable state access through to their callback without re-acquiring the mutex.nosync_publishAndApply(objectMessages:coreSDK:mutableStateCallback:)overload whose callback receivesinout MutableState, used bycreateCounter/createMapto look up newly-created objects without re-acquiring the mutex.This avoids the exclusive access violation but means the callback runs inside the mutex scope, relying on the callers (which are all
asyncfunctions usingCheckedContinuation) to naturally defer further work past the mutex boundary.Proposed fix
Move the behavioural methods listed above from
MutableStatetoInternalDefaultRealtimeObjects, restoringMutableStateto its intended role as a data store with focused mutation helpers. This would eliminate the need for the workaround and make the mutex usage straightforward.┆Issue is synchronized with this Jira Task by Unito