Skip to content

Move behavioural methods off MutableState to allow granular mutex control #120

@lawrence-forooghian

Description

@lawrence-forooghian

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

  1. withoutSync { /* state transitions */ }
  2. withoutSync { /* drain waiters, apply synthetic messages, collect callbacks */ }
  3. 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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions