@file:JvmMultifileClass @file:JvmName("FlowKt") package kotlinx.coroutines.flow import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.internal.* import kotlinx.coroutines.selects.* import kotlin.jvm.* import kotlin.time.* /* Scaffolding for Knit code examples */ /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * Example: * * ```kotlin * flow { * emit(1) * delay(90) * emit(2) * delay(90) * emit(3) * delay(1010) * emit(4) * delay(1010) * emit(5) * }.debounce(1000) * ``` * * * produces the following emissions * * ```text * 3, 4, 5 * ``` * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. */ @FlowPreview public fun Flow.debounce(timeoutMillis: Long): Flow { require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } if (timeoutMillis == 0L) return this return debounceInternal { timeoutMillis } } /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout][timeoutMillis]. * The latest value is always emitted. * * A variation of [debounce] that allows specifying the timeout value dynamically. * * Example: * * ```kotlin * flow { * emit(1) * delay(90) * emit(2) * delay(90) * emit(3) * delay(1010) * emit(4) * delay(1010) * emit(5) * }.debounce { * if (it == 1) { * 0L * } else { * 1000L * } * } * ``` * * * produces the following emissions * * ```text * 1, 3, 4, 5 * ``` * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeoutMillis] milliseconds. * * @param timeoutMillis [T] is the emitted value and the return value is timeout in milliseconds. */ @FlowPreview @OverloadResolutionByLambdaReturnType public fun Flow.debounce(timeoutMillis: (T) -> Long): Flow = debounceInternal(timeoutMillis) /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout]. * The latest value is always emitted. * * Example: * * ```kotlin * flow { * emit(1) * delay(90.milliseconds) * emit(2) * delay(90.milliseconds) * emit(3) * delay(1010.milliseconds) * emit(4) * delay(1010.milliseconds) * emit(5) * }.debounce(1000.milliseconds) * ``` * * * produces the following emissions * * ```text * 3, 4, 5 * ``` * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] milliseconds. */ @FlowPreview public fun Flow.debounce(timeout: Duration): Flow = debounce(timeout.toDelayMillis()) /** * Returns a flow that mirrors the original flow, but filters out values * that are followed by the newer values within the given [timeout]. * The latest value is always emitted. * * A variation of [debounce] that allows specifying the timeout value dynamically. * * Example: * * ```kotlin * flow { * emit(1) * delay(90.milliseconds) * emit(2) * delay(90.milliseconds) * emit(3) * delay(1010.milliseconds) * emit(4) * delay(1010.milliseconds) * emit(5) * }.debounce { * if (it == 1) { * 0.milliseconds * } else { * 1000.milliseconds * } * } * ``` * * * produces the following emissions * * ```text * 1, 3, 4, 5 * ``` * * * Note that the resulting flow does not emit anything as long as the original flow emits * items faster than every [timeout] unit. * * @param timeout [T] is the emitted value and the return value is timeout in [Duration]. */ @FlowPreview @JvmName("debounceDuration") @OverloadResolutionByLambdaReturnType public fun Flow.debounce(timeout: (T) -> Duration): Flow = debounceInternal { emittedItem -> timeout(emittedItem).toDelayMillis() } private fun Flow.debounceInternal(timeoutMillisSelector: (T) -> Long): Flow = scopedFlow { downstream -> // Produce the values using the default (rendezvous) channel val values = produce { collect { value -> send(value ?: NULL) } } // Now consume the values var lastValue: Any? = null while (lastValue !== DONE) { var timeoutMillis = 0L // will be always computed when lastValue != null // Compute timeout for this value if (lastValue != null) { timeoutMillis = timeoutMillisSelector(NULL.unbox(lastValue)) require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } if (timeoutMillis == 0L) { downstream.emit(NULL.unbox(lastValue)) lastValue = null // Consume the value } } // assert invariant: lastValue != null implies timeoutMillis > 0 assert { lastValue == null || timeoutMillis > 0 } // wait for the next value with timeout select { // Set timeout when lastValue exists and is not consumed yet if (lastValue != null) { onTimeout(timeoutMillis) { downstream.emit(NULL.unbox(lastValue)) lastValue = null // Consume the value } } values.onReceiveCatching { value -> value .onSuccess { lastValue = it } .onFailure { it?.let { throw it } // If closed normally, emit the latest value if (lastValue != null) downstream.emit(NULL.unbox(lastValue)) lastValue = DONE } } } } } /** * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis]. * * Example: * * ```kotlin * flow { * repeat(10) { * emit(it) * delay(110) * } * }.sample(200) * ``` * * * produces the following emissions * * ```text * 1, 3, 5, 7, 9 * ``` * * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview public fun Flow.sample(periodMillis: Long): Flow { require(periodMillis > 0) { "Sample period should be positive" } return scopedFlow { downstream -> val values = produce(capacity = Channel.CONFLATED) { collect { value -> send(value ?: NULL) } } var lastValue: Any? = null val ticker = fixedPeriodTicker(periodMillis) while (lastValue !== DONE) { select { values.onReceiveCatching { result -> result .onSuccess { lastValue = it } .onFailure { it?.let { throw it } ticker.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)) } } } } } /* * TODO this design (and design of the corresponding operator) depends on #540 */ internal fun CoroutineScope.fixedPeriodTicker( delayMillis: Long, ): ReceiveChannel { return produce(capacity = 0) { delay(delayMillis) while (true) { channel.send(Unit) delay(delayMillis) } } } /** * Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period]. * * Example: * * ```kotlin * flow { * repeat(10) { * emit(it) * delay(110.milliseconds) * } * }.sample(200.milliseconds) * ``` * * * produces the following emissions * * ```text * 1, 3, 5, 7, 9 * ``` * * * Note that the latest element is not emitted if it does not fit into the sampling window. */ @FlowPreview public fun Flow.sample(period: Duration): Flow = sample(period.toDelayMillis()) /** * Returns a flow that will emit a [TimeoutCancellationException] if the upstream doesn't emit an item within the given time. * * Example: * * ```kotlin * flow { * emit(1) * delay(100) * emit(2) * delay(100) * emit(3) * delay(1000) * emit(4) * }.timeout(100.milliseconds).catch { exception -> * if (exception is TimeoutCancellationException) { * // Catch the TimeoutCancellationException emitted above. * // Emit desired item on timeout. * emit(-1) * } else { * // Throw other exceptions. * throw exception * } * }.onEach { * delay(300) // This will not cause a timeout * } * ``` * * * produces the following emissions * * ```text * 1, 2, 3, -1 * ``` * * * Note that delaying on the downstream doesn't trigger the timeout. * * @param timeout Timeout duration. If non-positive, the flow is timed out immediately */ @FlowPreview public fun Flow.timeout( timeout: Duration ): Flow = timeoutInternal(timeout) private fun Flow.timeoutInternal( timeout: Duration ): Flow = scopedFlow { downStream -> if (timeout <= Duration.ZERO) throw TimeoutCancellationException("Timed out immediately") val values = buffer(Channel.RENDEZVOUS).produceIn(this) whileSelect { values.onReceiveCatching { value -> value.onSuccess { downStream.emit(it) }.onClosed { it?.let { throw it } return@onReceiveCatching false } return@onReceiveCatching true } onTimeout(timeout) { throw TimeoutCancellationException("Timed out waiting for $timeout") } } }