Provide atomic way to transform MutableStateFlow's state
See original GitHub issueSome state updates require to know the current state. A simple example is a StateFlow<List<T>>
which we want to maintain by subscribing to created
/deleted
/modified
events about these T
objects.
For each event, we need to read the existing List
in the state, and set the state to the new modified list.
My current use case is the following (uses a map, but the idea is the same):
suspend fun ChromeBrowserSession.watchTargetsIn(coroutineScope: CoroutineScope): StateFlow<Map<TargetID, TargetInfo>> {
val targetsFlow = MutableStateFlow(emptyMap<TargetID, TargetInfo>())
target.events().onEach { targetsFlow.value = targetsFlow.value.updatedBy(it) }.launchIn(coroutineScope)
// triggers target info events
target.setDiscoverTargets(SetDiscoverTargetsRequest(discover = true))
return targetsFlow
}
private fun Map<TargetID, TargetInfo>.updatedBy(event: TargetEvent): Map<TargetID, TargetInfo> = when (event) {
is TargetEvent.TargetCreatedEvent -> this + (event.targetInfo.targetId to event.targetInfo)
is TargetEvent.TargetInfoChangedEvent -> this + (event.targetInfo.targetId to event.targetInfo)
is TargetEvent.TargetDestroyedEvent -> this - event.targetId
is TargetEvent.TargetCrashedEvent -> this - event.targetId
}
This is all fine, but sometimes we may want the MutableStateFlow
to be accessed concurrently from multiple places.
I used to have separate subscription methods for different kinds of events, and had to use 4 coroutines to subscribe to each kind until I created a different API for all events()
.
In that case, I had to externally synchronize operations to ensure that I would read and write the state atomically, value = value.updatedBy(event)
doesn’t really work on its own.
There is already compareAndSet
which provides this kind of guarantees, but I wish there were a transform
method like:
/** Atomically sets the value of this `MutableStateFlow` by applying the given [transform] on the current state. */
fun <T> MutableStateFlow<T>.transformValue(transform: (currentState: T) -> T)
Is there already something like this? Am I missing something here?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:14 (14 by maintainers)
Top GitHub Comments
Very clear explanation, thanks a lot. Yes, this implementation has problems, that’s why I was thinking that having something smarter in the library would prevent wrong user-implementations like this. But I guess there is no simple solution to this, so I guess I understand why you prefer not to provide anything rather than providing some risky primitive.
Again thanks for your time explaining this. Please feel free to close this issue.
Some context:
StateFlow
andSharedFlow
are usingsynchornized(this)
as an implementation detail. Synchronization there is fast and fine-grained and no coroutines/collectors are ever resumed under the lock. This is just more faster/simpler alternative to lock-free implementation (taking into account all the constraints).Now the problem with
transformAndSet
is that now we expose the primitive that invokes a user-supplied arbitrarytransform
that is executed under the lock. And this lambda can block indefinitely, be really slow, execute disk IO etc. All of that while holding the lock (-> preventing the progress of all emitters and collectors). While it may work for you, because you may be aware of these limitations, such API exposed as a general-purpose one is just a time-bomb waiting to explode. So I’d suggest you to stick with your extension method, but it’s unlikely that we want incorporate it in the default API