Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample operator to Flow, which can sample a Flow with the emissions of another Flow #1153

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5aa235b
Add sample for channels and flows
adibfara Apr 27, 2019
4a39779
Add tests for flow
adibfara Apr 28, 2019
cb4ae3c
Add tests for flow with another flow/channel
adibfara Apr 28, 2019
52f68e6
Merge remote-tracking branch 'origin/develop' into flow-sample-refactor
adibfara Apr 28, 2019
9c94161
Remove sampleBy with a ReceiveChannel since they can easily be conver…
adibfara Apr 28, 2019
3299857
Remove sampleBy with a ReceiveChannel since they can easily be conver…
adibfara Apr 28, 2019
1ddf6e4
Merge remote-tracking branch 'origin/master' into flow-sample-refactor
adibfara Jun 8, 2019
c1176f2
Merge pull request #1 from Kotlin/master
adibfara Jun 8, 2019
0b05d11
Merge branch 'master' into flow-sample-refactor
adibfara Jun 8, 2019
2aac5fc
Update sample usage to use `flowScope`
adibfara Jun 8, 2019
d2ecd7d
Update sample usage to use `flowScope`
adibfara Jun 8, 2019
a172c48
Merge pull request #3 from Kotlin/master
adibfara Jul 12, 2020
26a10a2
Merge branch 'master' into flow-sample-refactor
adibfara Jul 12, 2020
c21baec
Update unit tests
adibfara Jul 12, 2020
b668684
Merge remote-tracking branch 'upstream/master' into flow-sample-refactor
adibfara Sep 25, 2020
f965229
Merge branch 'Kotlin:master' into master
adibfara Jul 11, 2023
3082c60
Merge branch 'master' into flow-sample-refactor
AFXtapsi Jul 11, 2023
a45f5fd
Update logic with master branch
AFXtapsi Jul 11, 2023
7b2bfee
Update logic with master branch
AFXtapsi Jul 11, 2023
7812240
Merge remote-tracking branch 'origin/flow-sample-refactor' into flow-…
AFXtapsi Jul 11, 2023
5d23c0d
Update logic with master branch
AFXtapsi Jul 11, 2023
4a0107a
Update logic with master
Jul 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Expand Up @@ -1105,7 +1105,7 @@ public final class kotlinx/coroutines/flow/FlowKt {
public static final fun retryWhen (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow;
public static final fun runningFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
public static final fun runningReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
public static final fun sample (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
public static final fun sample (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
public static final fun sample-HG0u8IE (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
Expand Down
69 changes: 59 additions & 10 deletions kotlinx-coroutines-core/common/src/flow/operators/Delay.kt
Expand Up @@ -203,7 +203,7 @@ public fun <T> Flow<T>.debounce(timeout: (T) -> Duration): Flow<T> =
timeout(emittedItem).toDelayMillis()
}

private fun <T> Flow<T>.debounceInternal(timeoutMillisSelector: (T) -> Long) : Flow<T> =
private fun <T> Flow<T>.debounceInternal(timeoutMillisSelector: (T) -> Long): Flow<T> =
scopedFlow { downstream ->
// Produce the values using the default (rendezvous) channel
val values = produce {
Expand Down Expand Up @@ -273,30 +273,76 @@ private fun <T> Flow<T>.debounceInternal(timeoutMillisSelector: (T) -> Long) : F
*/
@FlowPreview
public fun <T> Flow<T>.sample(periodMillis: Long): Flow<T> {
require(periodMillis > 0) { "Sample period should be positive" }
return sample(flow {
delay(periodMillis)
while (true) {
emit(Unit)
delay(periodMillis)
}
})
}

/**
* Returns a flow that emits only the latest value emitted by the original flow only when the [sampler] emits.
*
* Example:
* ```
* flow {
* repeat(10) {
* emit(it)
* delay(50)
* }
* }.sampleBy(flow {
* repeat(10) {
* delay(100)
* emit(it)
* }
* })
*
* ```
* produces `0, 2, 4, 6, 8`.
*
* Note that the latest element is not emitted if it does not fit into the sampling window.
*/
public fun <T, R> Flow<T>.sample(sampler: Flow<R>): Flow<T> {
return scopedFlow { downstream ->
val values = produce(capacity = Channel.CONFLATED) {
collect { value -> send(value ?: NULL) }
}

val samplerProducer = produce(capacity = 0) {
sampler.collect { value ->
send(value)
}
}
var lastValue: Any? = null
val ticker = fixedPeriodTicker(periodMillis)
while (lastValue !== DONE) {
select<Unit> {
values.onReceiveCatching { result ->
result
.onSuccess { lastValue = it }
.onFailure {
it?.let { throw it }
ticker.cancel(ChildCancelledException())
samplerProducer.cancel(ChildCancelledException())
lastValue = DONE
}
}

// todo: shall be start sampling only when an element arrives or sample aways as here?
ticker.onReceive {
val value = lastValue ?: return@onReceive
lastValue = null // Consume the value
downstream.emit(NULL.unbox(value))
samplerProducer.onReceiveCatching { samplerResult ->
samplerResult
.onSuccess { sampledValue ->
if (sampledValue != null) {
val value = lastValue ?: return@onSuccess
lastValue = null // Consume the value
downstream.emit(NULL.unbox(value))
} else {
lastValue = DONE
}
}
.onFailure {
lastValue = DONE
}

}
}
}
Expand All @@ -306,7 +352,10 @@ public fun <T> Flow<T>.sample(periodMillis: Long): Flow<T> {
/*
* TODO this design (and design of the corresponding operator) depends on #540
*/
internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMillis: Long = delayMillis): ReceiveChannel<Unit> {
internal fun CoroutineScope.fixedPeriodTicker(
delayMillis: Long,
initialDelayMillis: Long = delayMillis
): ReceiveChannel<Unit> {
require(delayMillis >= 0) { "Expected non-negative delay, but has $delayMillis ms" }
require(initialDelayMillis >= 0) { "Expected non-negative initial delay, but has $initialDelayMillis ms" }
return produce(capacity = 0) {
Expand Down