Skip to content

Commit 92db4e1

Browse files
elizarovmkano9qwwdfsad
authored
Add debounce with selector and kotlin.time (#2336)
Co-authored-by: Miguel Kano <[email protected]> Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
1 parent 53f007f commit 92db4e1

File tree

8 files changed

+343
-50
lines changed

8 files changed

+343
-50
lines changed

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,9 @@ public final class kotlinx/coroutines/flow/FlowKt {
942942
public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
943943
public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
944944
public static final fun debounce (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
945+
public static final fun debounce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
945946
public static final fun debounce-8GFy2Ro (Lkotlinx/coroutines/flow/Flow;D)Lkotlinx/coroutines/flow/Flow;
947+
public static final fun debounceDuration (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
946948
public static final fun delayEach (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
947949
public static final fun delayFlow (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
948950
public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;

kotlinx-coroutines-core/common/src/flow/operators/Delay.kt

Lines changed: 152 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -64,37 +64,59 @@ fun main() = runBlocking {
6464
*/
6565
@FlowPreview
6666
public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T> {
67-
require(timeoutMillis > 0) { "Debounce timeout should be positive" }
68-
return scopedFlow { downstream ->
69-
// Actually Any, KT-30796
70-
val values = produce<Any?>(capacity = Channel.CONFLATED) {
71-
collect { value -> send(value ?: NULL) }
72-
}
73-
var lastValue: Any? = null
74-
while (lastValue !== DONE) {
75-
select<Unit> {
76-
// Should be receiveOrClosed when boxing issues are fixed
77-
values.onReceiveOrNull {
78-
if (it == null) {
79-
if (lastValue != null) downstream.emit(NULL.unbox(lastValue))
80-
lastValue = DONE
81-
} else {
82-
lastValue = it
83-
}
84-
}
85-
86-
lastValue?.let { value ->
87-
// set timeout when lastValue != null
88-
onTimeout(timeoutMillis) {
89-
lastValue = null // Consume the value
90-
downstream.emit(NULL.unbox(value))
91-
}
92-
}
93-
}
94-
}
95-
}
67+
require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" }
68+
if (timeoutMillis == 0L) return this
69+
return debounceInternal { timeoutMillis }
9670
}
9771

72+
/**
73+
* Returns a flow that mirrors the original flow, but filters out values
74+
* that are followed by the newer values within the given [timeout][timeoutMillis].
75+
* The latest value is always emitted.
76+
*
77+
* A variation of [debounce] that allows specifying the timeout value dynamically.
78+
*
79+
* Example:
80+
*
81+
* ```kotlin
82+
* flow {
83+
* emit(1)
84+
* delay(90)
85+
* emit(2)
86+
* delay(90)
87+
* emit(3)
88+
* delay(1010)
89+
* emit(4)
90+
* delay(1010)
91+
* emit(5)
92+
* }.debounce {
93+
* if (it == 1) {
94+
* 0L
95+
* } else {
96+
* 1000L
97+
* }
98+
* }
99+
* ```
100+
* <!--- KNIT example-delay-02.kt -->
101+
*
102+
* produces the following emissions
103+
*
104+
* ```text
105+
* 1, 3, 4, 5
106+
* ```
107+
* <!--- TEST -->
108+
*
109+
* Note that the resulting flow does not emit anything as long as the original flow emits
110+
* items faster than every [timeoutMillis] milliseconds.
111+
*
112+
* @param timeoutMillis [T] is the emitted value and the return value is timeout in milliseconds.
113+
*/
114+
@FlowPreview
115+
@OptIn(kotlin.experimental.ExperimentalTypeInference::class)
116+
@OverloadResolutionByLambdaReturnType
117+
public fun <T> Flow<T>.debounce(timeoutMillis: (T) -> Long): Flow<T> =
118+
debounceInternal(timeoutMillis)
119+
98120
/**
99121
* Returns a flow that mirrors the original flow, but filters out values
100122
* that are followed by the newer values within the given [timeout].
@@ -129,7 +151,104 @@ public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T> {
129151
*/
130152
@ExperimentalTime
131153
@FlowPreview
132-
public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> = debounce(timeout.toDelayMillis())
154+
public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> =
155+
debounce(timeout.toDelayMillis())
156+
157+
/**
158+
* Returns a flow that mirrors the original flow, but filters out values
159+
* that are followed by the newer values within the given [timeout].
160+
* The latest value is always emitted.
161+
*
162+
* A variation of [debounce] that allows specifying the timeout value dynamically.
163+
*
164+
* Example:
165+
*
166+
* ```kotlin
167+
* flow {
168+
* emit(1)
169+
* delay(90.milliseconds)
170+
* emit(2)
171+
* delay(90.milliseconds)
172+
* emit(3)
173+
* delay(1010.milliseconds)
174+
* emit(4)
175+
* delay(1010.milliseconds)
176+
* emit(5)
177+
* }.debounce {
178+
* if (it == 1) {
179+
* 0.milliseconds
180+
* } else {
181+
* 1000.milliseconds
182+
* }
183+
* }
184+
* ```
185+
* <!--- KNIT example-delay-duration-02.kt -->
186+
*
187+
* produces the following emissions
188+
*
189+
* ```text
190+
* 1, 3, 4, 5
191+
* ```
192+
* <!--- TEST -->
193+
*
194+
* Note that the resulting flow does not emit anything as long as the original flow emits
195+
* items faster than every [timeout] unit.
196+
*
197+
* @param timeout [T] is the emitted value and the return value is timeout in [Duration].
198+
*/
199+
@ExperimentalTime
200+
@FlowPreview
201+
@JvmName("debounceDuration")
202+
@OptIn(kotlin.experimental.ExperimentalTypeInference::class)
203+
@OverloadResolutionByLambdaReturnType
204+
public fun <T> Flow<T>.debounce(timeout: (T) -> Duration): Flow<T> =
205+
debounceInternal { emittedItem ->
206+
timeout(emittedItem).toDelayMillis()
207+
}
208+
209+
private fun <T> Flow<T>.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow<T> =
210+
scopedFlow { downstream ->
211+
// Produce the values using the default (rendezvous) channel
212+
// Note: the actual type is Any, KT-30796
213+
val values = produce<Any?> {
214+
collect { value -> send(value ?: NULL) }
215+
}
216+
// Now consume the values
217+
var lastValue: Any? = null
218+
while (lastValue !== DONE) {
219+
var timeoutMillis = 0L // will be always computed when lastValue != null
220+
// Compute timeout for this value
221+
if (lastValue != null) {
222+
timeoutMillis = timeoutMillisSelector(NULL.unbox(lastValue))
223+
require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" }
224+
if (timeoutMillis == 0L) {
225+
downstream.emit(NULL.unbox(lastValue))
226+
lastValue = null // Consume the value
227+
}
228+
}
229+
// assert invariant: lastValue != null implies timeoutMillis > 0
230+
assert { lastValue == null || timeoutMillis > 0 }
231+
// wait for the next value with timeout
232+
select<Unit> {
233+
// Set timeout when lastValue exists and is not consumed yet
234+
if (lastValue != null) {
235+
onTimeout(timeoutMillis) {
236+
downstream.emit(NULL.unbox(lastValue))
237+
lastValue = null // Consume the value
238+
}
239+
}
240+
// Should be receiveOrClosed when boxing issues are fixed
241+
values.onReceiveOrNull { value ->
242+
if (value == null) {
243+
if (lastValue != null) downstream.emit(NULL.unbox(lastValue))
244+
lastValue = DONE
245+
} else {
246+
lastValue = value
247+
}
248+
}
249+
}
250+
}
251+
}
133252

134253
/**
135254
* Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis].
@@ -144,15 +263,15 @@ public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> = debounce(timeout.t
144263
* }
145264
* }.sample(200)
146265
* ```
147-
* <!--- KNIT example-delay-02.kt -->
266+
* <!--- KNIT example-delay-03.kt -->
148267
*
149268
* produces the following emissions
150269
*
151270
* ```text
152271
* 1, 3, 5, 7, 9
153272
* ```
154273
* <!--- TEST -->
155-
*
274+
*
156275
* Note that the latest element is not emitted if it does not fit into the sampling window.
157276
*/
158277
@FlowPreview
@@ -215,7 +334,7 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil
215334
* }
216335
* }.sample(200.milliseconds)
217336
* ```
218-
* <!--- KNIT example-delay-duration-02.kt -->
337+
* <!--- KNIT example-delay-duration-03.kt -->
219338
*
220339
* produces the following emissions
221340
*

kotlinx-coroutines-core/common/test/flow/operators/DebounceTest.kt

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.time.*
1111

1212
class DebounceTest : TestBase() {
1313
@Test
14-
public fun testBasic() = withVirtualTime {
14+
fun testBasic() = withVirtualTime {
1515
expect(1)
1616
val flow = flow {
1717
expect(3)
@@ -159,7 +159,7 @@ class DebounceTest : TestBase() {
159159
expect(2)
160160
throw TestException()
161161
}.flowOn(NamedDispatchers("source")).debounce(Long.MAX_VALUE).map {
162-
expectUnreached()
162+
expectUnreached()
163163
}
164164
assertFailsWith<TestException>(flow)
165165
finish(3)
@@ -175,7 +175,6 @@ class DebounceTest : TestBase() {
175175
expect(2)
176176
yield()
177177
throw TestException()
178-
it
179178
}
180179

181180
assertFailsWith<TestException>(flow)
@@ -193,7 +192,6 @@ class DebounceTest : TestBase() {
193192
expect(2)
194193
yield()
195194
throw TestException()
196-
it
197195
}
198196

199197
assertFailsWith<TestException>(flow)
@@ -202,7 +200,7 @@ class DebounceTest : TestBase() {
202200

203201
@ExperimentalTime
204202
@Test
205-
public fun testDurationBasic() = withVirtualTime {
203+
fun testDurationBasic() = withVirtualTime {
206204
expect(1)
207205
val flow = flow {
208206
expect(3)
@@ -223,4 +221,102 @@ class DebounceTest : TestBase() {
223221
assertEquals(listOf("A", "D", "E"), result)
224222
finish(5)
225223
}
224+
225+
@ExperimentalTime
226+
@Test
227+
fun testDebounceSelectorBasic() = withVirtualTime {
228+
expect(1)
229+
val flow = flow {
230+
expect(3)
231+
emit(1)
232+
delay(90)
233+
emit(2)
234+
delay(90)
235+
emit(3)
236+
delay(1010)
237+
emit(4)
238+
delay(1010)
239+
emit(5)
240+
expect(4)
241+
}
242+
243+
expect(2)
244+
val result = flow.debounce {
245+
if (it == 1) {
246+
0
247+
} else {
248+
1000
249+
}
250+
}.toList()
251+
252+
assertEquals(listOf(1, 3, 4, 5), result)
253+
finish(5)
254+
}
255+
256+
@Test
257+
fun testZeroDebounceTime() = withVirtualTime {
258+
expect(1)
259+
val flow = flow {
260+
expect(3)
261+
emit("A")
262+
emit("B")
263+
emit("C")
264+
expect(4)
265+
}
266+
267+
expect(2)
268+
val result = flow.debounce(0).toList()
269+
270+
assertEquals(listOf("A", "B", "C"), result)
271+
finish(5)
272+
}
273+
274+
@ExperimentalTime
275+
@Test
276+
fun testZeroDebounceTimeSelector() = withVirtualTime {
277+
expect(1)
278+
val flow = flow {
279+
expect(3)
280+
emit("A")
281+
emit("B")
282+
expect(4)
283+
}
284+
285+
expect(2)
286+
val result = flow.debounce { 0 }.toList()
287+
288+
assertEquals(listOf("A", "B"), result)
289+
finish(5)
290+
}
291+
292+
@ExperimentalTime
293+
@Test
294+
fun testDebounceDurationSelectorBasic() = withVirtualTime {
295+
expect(1)
296+
val flow = flow {
297+
expect(3)
298+
emit("A")
299+
delay(1500.milliseconds)
300+
emit("B")
301+
delay(500.milliseconds)
302+
emit("C")
303+
delay(250.milliseconds)
304+
emit("D")
305+
delay(2000.milliseconds)
306+
emit("E")
307+
expect(4)
308+
}
309+
310+
expect(2)
311+
val result = flow.debounce {
312+
if (it == "C") {
313+
0.milliseconds
314+
} else {
315+
1000.milliseconds
316+
}
317+
}.toList()
318+
319+
assertEquals(listOf("A", "C", "D", "E"), result)
320+
finish(5)
321+
}
226322
}

0 commit comments

Comments
 (0)