Skip to content

Commit 1e7ca75

Browse files
elizarovfvasco
andauthored
kotlin.time.Duration support (#1811)
Fixes #1402 Co-authored-by: Francesco Vasco <[email protected]>
1 parent 5cfd6c0 commit 1e7ca75

File tree

11 files changed

+794
-3
lines changed

11 files changed

+794
-3
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ public final class kotlinx/coroutines/Delay$DefaultImpls {
267267

268268
public final class kotlinx/coroutines/DelayKt {
269269
public static final fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
270+
public static final fun delay-p9JZ4hM (DLkotlin/coroutines/Continuation;)Ljava/lang/Object;
270271
}
271272

272273
public final class kotlinx/coroutines/DispatchedContinuationKt {
@@ -527,7 +528,9 @@ public final class kotlinx/coroutines/TimeoutCancellationException : java/util/c
527528

528529
public final class kotlinx/coroutines/TimeoutKt {
529530
public static final fun withTimeout (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
531+
public static final fun withTimeout-lwyi7ZQ (DLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
530532
public static final fun withTimeoutOrNull (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
533+
public static final fun withTimeoutOrNull-lwyi7ZQ (DLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
531534
}
532535

533536
public final class kotlinx/coroutines/YieldKt {
@@ -888,6 +891,7 @@ public final class kotlinx/coroutines/flow/FlowKt {
888891
public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
889892
public static final fun count (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
890893
public static final fun debounce (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
894+
public static final fun debounce-8GFy2Ro (Lkotlinx/coroutines/flow/Flow;D)Lkotlinx/coroutines/flow/Flow;
891895
public static final fun delayEach (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
892896
public static final fun delayFlow (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
893897
public static final fun distinctUntilChanged (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
@@ -954,6 +958,7 @@ public final class kotlinx/coroutines/flow/FlowKt {
954958
public static synthetic fun retry$default (Lkotlinx/coroutines/flow/Flow;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
955959
public static final fun retryWhen (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function4;)Lkotlinx/coroutines/flow/Flow;
956960
public static final fun sample (Lkotlinx/coroutines/flow/Flow;J)Lkotlinx/coroutines/flow/Flow;
961+
public static final fun sample-8GFy2Ro (Lkotlinx/coroutines/flow/Flow;D)Lkotlinx/coroutines/flow/Flow;
957962
public static final fun scan (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
958963
public static final fun scanFold (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
959964
public static final fun scanReduce (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function3;)Lkotlinx/coroutines/flow/Flow;
@@ -1092,6 +1097,7 @@ public abstract interface class kotlinx/coroutines/selects/SelectInstance {
10921097
}
10931098

10941099
public final class kotlinx/coroutines/selects/SelectKt {
1100+
public static final fun onTimeout-0lHKgQg (Lkotlinx/coroutines/selects/SelectBuilder;DLkotlin/jvm/functions/Function1;)V
10951101
public static final fun select (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
10961102
}
10971103

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

+22
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines
66

77
import kotlinx.coroutines.selects.*
88
import kotlin.coroutines.*
9+
import kotlin.time.*
910

1011
/**
1112
* This dispatcher _feature_ is implemented by [CoroutineDispatcher] implementations that natively support
@@ -75,5 +76,26 @@ public suspend fun delay(timeMillis: Long) {
7576
}
7677
}
7778

79+
/**
80+
* Delays coroutine for a given [duration] without blocking a thread and resumes it after the specified time.
81+
* This suspending function is cancellable.
82+
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
83+
* immediately resumes with [CancellationException].
84+
*
85+
* Note that delay can be used in [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
86+
*
87+
* Implementation note: how exactly time is tracked is an implementation detail of [CoroutineDispatcher] in the context.
88+
*/
89+
@ExperimentalTime
90+
public suspend fun delay(duration: Duration) = delay(duration.toDelayMillis())
91+
7892
/** Returns [Delay] implementation of the given context */
7993
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
94+
95+
/**
96+
* Convert this duration to its millisecond value.
97+
* Positive durations are coerced at least `1`.
98+
*/
99+
@ExperimentalTime
100+
internal fun Duration.toDelayMillis(): Long =
101+
if (this > Duration.ZERO) toLongMilliseconds().coerceAtLeast(1) else 0

kotlinx-coroutines-core/common/src/Timeout.kt

+33
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.selects.*
1010
import kotlin.coroutines.*
1111
import kotlin.coroutines.intrinsics.*
1212
import kotlin.jvm.*
13+
import kotlin.time.*
1314

1415
/**
1516
* Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
@@ -32,6 +33,22 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
3233
}
3334
}
3435

36+
/**
37+
* Runs a given suspending [block] of code inside a coroutine with the specified [timeout] and throws
38+
* a [TimeoutCancellationException] if the timeout was exceeded.
39+
*
40+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
41+
* the cancellable suspending function inside the block throws a [TimeoutCancellationException].
42+
*
43+
* The sibling function that does not throw an exception on timeout is [withTimeoutOrNull].
44+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
45+
*
46+
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
47+
*/
48+
@ExperimentalTime
49+
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T =
50+
withTimeout(timeout.toDelayMillis(), block)
51+
3552
/**
3653
* Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns
3754
* `null` if this timeout was exceeded.
@@ -65,6 +82,22 @@ public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend Corout
6582
}
6683
}
6784

85+
/**
86+
* Runs a given suspending block of code inside a coroutine with the specified [timeout] and returns
87+
* `null` if this timeout was exceeded.
88+
*
89+
* The code that is executing inside the [block] is cancelled on timeout and the active or next invocation of
90+
* cancellable suspending function inside the block throws a [TimeoutCancellationException].
91+
*
92+
* The sibling function that throws an exception on timeout is [withTimeout].
93+
* Note that the timeout action can be specified for a [select] invocation with [onTimeout][SelectBuilder.onTimeout] clause.
94+
*
95+
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
96+
*/
97+
@ExperimentalTime
98+
public suspend fun <T> withTimeoutOrNull(timeout: Duration, block: suspend CoroutineScope.() -> T): T? =
99+
withTimeoutOrNull(timeout.toDelayMillis(), block)
100+
68101
private fun <U, T: U> setupTimeout(
69102
coroutine: TimeoutCoroutine<U, T>,
70103
block: suspend CoroutineScope.() -> T

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

+49-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.coroutines.channels.*
1212
import kotlinx.coroutines.flow.internal.*
1313
import kotlinx.coroutines.selects.*
1414
import kotlin.jvm.*
15-
import kotlinx.coroutines.flow.internal.unsafeFlow as flow
15+
import kotlin.time.*
1616

1717
/**
1818
* Returns a flow that mirrors the original flow, but filters out values
@@ -71,6 +71,34 @@ public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T> {
7171
}
7272
}
7373

74+
/**
75+
* Returns a flow that mirrors the original flow, but filters out values
76+
* that are followed by the newer values within the given [timeout].
77+
* The latest value is always emitted.
78+
*
79+
* Example:
80+
* ```
81+
* flow {
82+
* emit(1)
83+
* delay(90.milliseconds)
84+
* emit(2)
85+
* delay(90.milliseconds)
86+
* emit(3)
87+
* delay(1010.milliseconds)
88+
* emit(4)
89+
* delay(1010.milliseconds)
90+
* emit(5)
91+
* }.debounce(1000.milliseconds)
92+
* ```
93+
* produces `3, 4, 5`.
94+
*
95+
* Note that the resulting flow does not emit anything as long as the original flow emits
96+
* items faster than every [timeout] milliseconds.
97+
*/
98+
@ExperimentalTime
99+
@FlowPreview
100+
public fun <T> Flow<T>.debounce(timeout: Duration): Flow<T> = debounce(timeout.toDelayMillis())
101+
74102
/**
75103
* Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period][periodMillis].
76104
*
@@ -133,3 +161,23 @@ internal fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMil
133161
}
134162
}
135163
}
164+
165+
/**
166+
* Returns a flow that emits only the latest value emitted by the original flow during the given sampling [period].
167+
*
168+
* Example:
169+
* ```
170+
* flow {
171+
* repeat(10) {
172+
* emit(it)
173+
* delay(50.milliseconds)
174+
* }
175+
* }.sample(100.milliseconds)
176+
* ```
177+
* produces `1, 3, 5, 7, 9`.
178+
*
179+
* Note that the latest element is not emitted if it does not fit into the sampling window.
180+
*/
181+
@ExperimentalTime
182+
@FlowPreview
183+
public fun <T> Flow<T>.sample(period: Duration): Flow<T> = sample(period.toDelayMillis())

kotlinx-coroutines-core/common/src/selects/Select.kt

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import kotlin.coroutines.*
1414
import kotlin.coroutines.intrinsics.*
1515
import kotlin.jvm.*
1616
import kotlin.native.concurrent.*
17+
import kotlin.time.*
1718

1819
/**
1920
* Scope for [select] invocation.
@@ -52,6 +53,17 @@ public interface SelectBuilder<in R> {
5253
public fun onTimeout(timeMillis: Long, block: suspend () -> R)
5354
}
5455

56+
/**
57+
* Clause that selects the given [block] after the specified [timeout] passes.
58+
* If timeout is negative or zero, [block] is selected immediately.
59+
*
60+
* **Note: This is an experimental api.** It may be replaced with light-weight timer/timeout channels in the future.
61+
*/
62+
@ExperimentalCoroutinesApi
63+
@ExperimentalTime
64+
public fun <R> SelectBuilder<R>.onTimeout(timeout: Duration, block: suspend () -> R) =
65+
onTimeout(timeout.toDelayMillis(), block)
66+
5567
/**
5668
* Clause for [select] expression without additional parameters that does not select any value.
5769
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED", "DEPRECATION")
6+
7+
// KT-21913
8+
9+
package kotlinx.coroutines
10+
11+
import kotlin.test.*
12+
import kotlin.time.*
13+
14+
@ExperimentalTime
15+
class DelayDurationTest : TestBase() {
16+
17+
@Test
18+
fun testCancellation() = runTest(expected = { it is CancellationException }) {
19+
runAndCancel(1.seconds)
20+
}
21+
22+
@Test
23+
fun testInfinite() = runTest(expected = { it is CancellationException }) {
24+
runAndCancel(Duration.INFINITE)
25+
}
26+
27+
@Test
28+
fun testRegularDelay() = runTest {
29+
val deferred = async {
30+
expect(2)
31+
delay(1.seconds)
32+
expect(4)
33+
}
34+
35+
expect(1)
36+
yield()
37+
expect(3)
38+
deferred.await()
39+
finish(5)
40+
}
41+
42+
@Test
43+
fun testNanoDelay() = runTest {
44+
val deferred = async {
45+
expect(2)
46+
delay(1.nanoseconds)
47+
expect(4)
48+
}
49+
50+
expect(1)
51+
yield()
52+
expect(3)
53+
deferred.await()
54+
finish(5)
55+
}
56+
57+
private suspend fun runAndCancel(time: Duration) = coroutineScope {
58+
expect(1)
59+
val deferred = async {
60+
expect(2)
61+
delay(time)
62+
expectUnreached()
63+
}
64+
65+
yield()
66+
expect(3)
67+
require(deferred.isActive)
68+
deferred.cancel()
69+
finish(4)
70+
deferred.await()
71+
}
72+
}

0 commit comments

Comments
 (0)