4
4
5
5
package kotlinx.coroutines.flow.internal
6
6
7
+ import kotlinx.coroutines.channels.*
7
8
import kotlinx.coroutines.flow.*
8
9
import kotlinx.coroutines.internal.*
9
10
import kotlin.coroutines.*
@@ -26,12 +27,12 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
26
27
protected var nCollectors = 0 // number of allocated (!free) slots
27
28
private set
28
29
private var nextIndex = 0 // oracle for the next free slot index
29
- private var _subscriptionCount : MutableStateFlow < Int > ? = null // init on first need
30
+ private var _subscriptionCount : SubscriptionCountStateFlow ? = null // init on first need
30
31
31
32
val subscriptionCount: StateFlow <Int >
32
33
get() = synchronized(this ) {
33
34
// allocate under lock in sync with nCollectors variable
34
- _subscriptionCount ? : MutableStateFlow (nCollectors).also {
35
+ _subscriptionCount ? : SubscriptionCountStateFlow (nCollectors).also {
35
36
_subscriptionCount = it
36
37
}
37
38
}
@@ -43,7 +44,7 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
43
44
@Suppress(" UNCHECKED_CAST" )
44
45
protected fun allocateSlot (): S {
45
46
// Actually create slot under lock
46
- var subscriptionCount: MutableStateFlow < Int > ? = null
47
+ var subscriptionCount: SubscriptionCountStateFlow ? = null
47
48
val slot = synchronized(this ) {
48
49
val slots = when (val curSlots = slots) {
49
50
null -> createSlotArray(2 ).also { slots = it }
@@ -74,7 +75,7 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
74
75
@Suppress(" UNCHECKED_CAST" )
75
76
protected fun freeSlot (slot : S ) {
76
77
// Release slot under lock
77
- var subscriptionCount: MutableStateFlow < Int > ? = null
78
+ var subscriptionCount: SubscriptionCountStateFlow ? = null
78
79
val resumes = synchronized(this ) {
79
80
nCollectors--
80
81
subscriptionCount = _subscriptionCount // retrieve under lock if initialized
@@ -83,10 +84,10 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
83
84
(slot as AbstractSharedFlowSlot <Any >).freeLocked(this )
84
85
}
85
86
/*
86
- Resume suspended coroutines.
87
- This can happens when the subscriber that was freed was a slow one and was holding up buffer.
88
- When this subscriber was freed, previously queued emitted can now wake up and are resumed here.
89
- */
87
+ * Resume suspended coroutines.
88
+ * This can happen when the subscriber that was freed was a slow one and was holding up buffer.
89
+ * When this subscriber was freed, previously queued emitted can now wake up and are resumed here.
90
+ */
90
91
for (cont in resumes) cont?.resume(Unit )
91
92
// decrement subscription count
92
93
subscriptionCount?.increment(- 1 )
@@ -99,3 +100,35 @@ internal abstract class AbstractSharedFlow<S : AbstractSharedFlowSlot<*>> : Sync
99
100
}
100
101
}
101
102
}
103
+
104
+ /* *
105
+ * [StateFlow] that represents the number of subscriptions.
106
+ *
107
+ * It is exposed as a regular [StateFlow] in our public API, but it is implemented as [SharedFlow] undercover to
108
+ * avoid conflations of consecutive updates because the subscription count is very sensitive to it.
109
+ *
110
+ * The importance of non-conflating can be demonstrated with the following example:
111
+ * ```
112
+ * val shared = flowOf(239).stateIn(this, SharingStarted.Lazily, 42) // stateIn for the sake of the initial value
113
+ * println(shared.first())
114
+ * yield()
115
+ * println(shared.first())
116
+ * ```
117
+ * If the flow is shared within the same dispatcher (e.g. Main) or with a slow/throttled one,
118
+ * the `SharingStarted.Lazily` will never be able to start the source: `first` sees the initial value and immediately
119
+ * unsubscribes, leaving the asynchronous `SharingStarted` with conflated zero.
120
+ *
121
+ * To avoid that (especially in a more complex scenarios), we do not conflate subscription updates.
122
+ */
123
+ private class SubscriptionCountStateFlow (initialValue : Int ) : StateFlow<Int>,
124
+ SharedFlowImpl <Int >(1 , Int .MAX_VALUE , BufferOverflow .DROP_OLDEST )
125
+ {
126
+ init { tryEmit(initialValue) }
127
+
128
+ override val value: Int
129
+ get() = synchronized(this ) { lastReplayedLocked }
130
+
131
+ fun increment (delta : Int ) = synchronized(this ) {
132
+ tryEmit(lastReplayedLocked + delta)
133
+ }
134
+ }
0 commit comments