I need a single manager that manages the desire of all usages, calling the data source based on the sum of all requirements, and feeding each usage the data it requested.
/**
* Constructs a [MultiplexFlow].
*
* Behavior:
* * [getAll] is called every time the total keys collected by flows returned by [MultiplexFlow.get] changes (when collection is started or stopped).
* * [getAll] is called with the total keys of all collected [MultiplexFlow.get] flows.
* * [MultiplexFlow.get] calls share the data between them, such that [getAll] is not invoked when all the keys provided to [MultiplexFlow.get] are already collected by another [MultiplexFlow.get] caller.
* If [replay] is 0, this rule does not apply and [getAll] is re-invoked for every change in collections.
* * Errors in calls to [getAll] trigger a rollback to the previous keys, and collections of all [MultiplexFlow.get] with one of the new keys will throw that error.
* * Follow-up [getAll] error, or an error after the [getAll] collection already succeeded, will clear all subscriptions and cause all [MultiplexFlow.get] collections to throw that error.
* * If the flow returned by [getAll] finishes, all current collections of [MultiplexFlow.get] finish as well, and follow-up collections will re-invoke [getAll].
*/
public fun <K, V> MultiplexFlow(
scope: CoroutineScope,
replay: Int = 1,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
getAll: suspend (keys: Set<K>) -> Flow<Map<K, V>>,
): MultiplexFlow<K, V>
/**
* Allows multiplexing multiple subscriptions to a single [Flow].
*
* This is useful when the source allows only a single subscription, but the data is needed by multiple users.
*/
public class MultiplexFlow<K, V> internal constructor(...) {
/** Returns a [Flow] that emits [V] for the requested [K]s, based on the map provided by `getAll`. */
public operator fun get(vararg keys: K): Flow<V>
}
//
// Sample usage:
//
val multiplexFlow = MultiplexFlow<Int, String>(scope) { keys: Set<Int> -> // keys of all requests, eventually {1, 2, 3}
// Collection of this flow will be cancelled when the set of total keys is replaced.
dataSourceFor(keys).map { values: List<DataValue> -> // values for all requests
values.associateBy { it.key } // mapping to allow each user to get only the data they requested
}
}
launch {
multiplexFlow[1, 2].collect { value -> /* values with keys 1 or 2 */ }
}
launch {
multiplexFlow[2, 3].collect { value -> /* values with keys 2 or 3 */ }
}
data class DataValue(key: Int, ...)
Prior Art
This is similar to a SharedFlow, except for these distinctions:
It is aware of subscription of specific data requested, rather than a single global requirement.
Each collector only gets the specific data it wants, rather than everything.
Implementing this is very error prone, around both thread safety and lifecycle (e.g. rollback when a new user requested data that fails to be fetched). It took many cycles in my project to get this right, so I thought it would be a good idea to have an implementation in kotlinx.coroutines.
Use case
set...Callback
rather thanadd...Callback
).Set
).The Shape of the API
Example implementation and API can be seen in https://github.com/Kotlin/kotlinx.coroutines/compare/master...odedniv:kotlinx.coroutines:multiplex. Generally:
Prior Art
This is similar to a
SharedFlow
, except for these distinctions:Because of these distinctions, there would have to be multiple
SharedFlow
s, which would be hard to get right if there's only a single data source for all of them. Note that the suggested implementation in https://github.com/Kotlin/kotlinx.coroutines/compare/master...odedniv:kotlinx.coroutines:multiplex is actually based on maintaining multipleSharedFlow
s, each feeding the specific users.Implementing this is very error prone, around both thread safety and lifecycle (e.g. rollback when a new user requested data that fails to be fetched). It took many cycles in my project to get this right, so I thought it would be a good idea to have an implementation in
kotlinx.coroutines
.