Skip to content

Commit b170819

Browse files
committed
MPP: Promise support in JS
1 parent a7db8ec commit b170819

File tree

5 files changed

+197
-10
lines changed

5 files changed

+197
-10
lines changed

integration/kotlinx-coroutines-jdk8/src/main/kotlin/kotlinx/coroutines/experimental/future/Future.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import kotlin.coroutines.experimental.CoroutineContext
2727
import kotlin.coroutines.experimental.suspendCoroutine
2828

2929
/**
30-
* Starts new coroutine and returns its results an an implementation of [CompletableFuture].
30+
* Starts new coroutine and returns its result as an implementation of [CompletableFuture].
3131
* This coroutine builder uses [CommonPool] context by default and is conceptually similar to [CompletableFuture.supplyAsync].
3232
*
3333
* The running coroutine is cancelled when the resulting future is cancelled or otherwise completed.
3434
*
3535
* The [context] for the new coroutine can be explicitly specified.
3636
* See [CoroutineDispatcher] for the standard context implementations that are provided by `kotlinx.coroutines`.
37-
* The [context][CoroutineScope.context] of the parent coroutine from its [scope][CoroutineScope] may be used,
37+
* The [context][CoroutineScope.coroutineContext] of the parent coroutine from its [scope][CoroutineScope] may be used,
3838
* in which case the [Job] of the resulting coroutine is a child of the job of the parent coroutine.
3939
* The parent job may be also explicitly specified using [parent] parameter.
4040
*

js/kotlinx-coroutines-core-js/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ internal class CancellableContinuationImpl<in T>(
259259

260260
override fun toString(): String =
261261
"CancellableContinuation{${stateString()}}[$delegate]"
262+
263+
// todo: This workaround for KT-21968, should be removed in the future
264+
public override fun cancel(cause: Throwable?): Boolean =
265+
super.cancel(cause)
262266
}
263267

264268
private class CompletedIdempotentResult(

js/kotlinx-coroutines-core-js/src/main/kotlin/kotlinx/coroutines/experimental/CoroutineContext.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package kotlinx.coroutines.experimental
1818

19-
import kotlin.browser.window
2019
import kotlin.coroutines.experimental.ContinuationInterceptor
2120
import kotlin.coroutines.experimental.CoroutineContext
2221

@@ -46,29 +45,29 @@ public actual val DefaultDispatcher: CoroutineDispatcher = DefaultExecutor
4645

4746
internal object DefaultExecutor : CoroutineDispatcher(), Delay {
4847
fun enqueue(block: Runnable) {
49-
window.setTimeout({ block.run() }, 0)
48+
setTimeout({ block.run() }, 0)
5049
}
5150

5251
fun schedule(time: Double, block: Runnable): Int =
53-
window.setTimeout({ block.run() }, time.timeToInt())
52+
setTimeout({ block.run() }, time.timeToInt())
5453

5554
fun removeScheduled(handle: Int) {
56-
window.clearTimeout(handle)
55+
clearTimeout(handle)
5756
}
5857

5958
override fun dispatch(context: CoroutineContext, block: Runnable) {
60-
window.setTimeout({ block.run() }, 0)
59+
setTimeout({ block.run() }, 0)
6160
}
6261

6362
override fun scheduleResumeAfterDelay(time: Int, continuation: CancellableContinuation<Unit>) {
64-
window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, time.coerceAtLeast(0))
63+
setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, time.coerceAtLeast(0))
6564
}
6665

6766
override fun invokeOnTimeout(time: Int, block: Runnable): DisposableHandle {
68-
val handle = window.setTimeout({ block.run() }, time.coerceAtLeast(0))
67+
val handle = setTimeout({ block.run() }, time.coerceAtLeast(0))
6968
return object : DisposableHandle {
7069
override fun dispose() {
71-
window.clearTimeout(handle)
70+
clearTimeout(handle)
7271
}
7372
}
7473
}
@@ -85,3 +84,9 @@ public fun newCoroutineContext(context: CoroutineContext, parent: Job? = null):
8584
return if (context !== DefaultDispatcher && context[ContinuationInterceptor] == null)
8685
wp + DefaultDispatcher else wp
8786
}
87+
88+
// We need to reference global setTimeout and clearTimeout so that it works on Node.JS as opposed to
89+
// using them via "window" (which only works in browser)
90+
91+
private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int
92+
private external fun clearTimeout(handle: Int = definedExternally): Unit
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2016-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package kotlinx.coroutines.experimental
18+
19+
import kotlin.coroutines.experimental.ContinuationInterceptor
20+
import kotlin.coroutines.experimental.CoroutineContext
21+
import kotlin.js.Promise
22+
23+
/**
24+
* Starts new coroutine and returns its result as an implementation of [Promise].
25+
*
26+
* The [context] for the new coroutine can be explicitly specified.
27+
* See [CoroutineDispatcher] for the standard context implementations that are provided by `kotlinx.coroutines`.
28+
* The [context][CoroutineScope.coroutineContext] of the parent coroutine from its [scope][CoroutineScope] may be used,
29+
* in which case the [Job] of the resulting coroutine is a child of the job of the parent coroutine.
30+
* The parent job may be also explicitly specified using [parent] parameter.
31+
*
32+
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [DefaultDispatcher] is used.
33+
*
34+
* By default, the coroutine is immediately scheduled for execution.
35+
* Other options can be specified via `start` parameter. See [CoroutineStart] for details.
36+
*
37+
* @param context context of the coroutine. The default value is [DefaultDispatcher].
38+
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
39+
* @param parent explicitly specifies the parent job, overrides job from the [context] (if any).
40+
* @param block the coroutine code.
41+
*/
42+
public fun <T> promise(
43+
context: CoroutineContext = DefaultDispatcher,
44+
start: CoroutineStart = CoroutineStart.DEFAULT,
45+
parent: Job? = null,
46+
block: suspend CoroutineScope.() -> T
47+
): Promise<T> =
48+
async(context, start, parent, block).asPromise()
49+
50+
/**
51+
* Converts this deferred value to the instance of [Promise].
52+
*/
53+
public fun <T> Deferred<T>.asPromise(): Promise<T> {
54+
val promise = Promise<T> { resolve, reject ->
55+
invokeOnCompletion {
56+
val e = getCompletionExceptionOrNull()
57+
if (e != null) {
58+
reject(e)
59+
} else {
60+
resolve(getCompleted())
61+
}
62+
}
63+
}
64+
promise.asDynamic().deferred = this
65+
return promise
66+
}
67+
68+
/**
69+
* Converts this promise value to the instance of [Deferred].
70+
*/
71+
public fun <T> Promise<T>.asDeferred(): Deferred<T> {
72+
val deferred = asDynamic().deferred
73+
@Suppress("UnsafeCastFromDynamic")
74+
return deferred ?: async { await() }
75+
}
76+
77+
/**
78+
* Awaits for completion of the promise without blocking.
79+
*
80+
* This suspending function is cancellable.
81+
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
82+
* stops waiting for the promise and immediately resumes with [CancellationException].
83+
*/
84+
public suspend fun <T> Promise<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
85+
this@await.then(
86+
onFulfilled = { cont.resume(it) },
87+
onRejected = { cont.resumeWithException(it) })
88+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2016-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package kotlinx.coroutines.experimental
18+
19+
import kotlin.js.Promise
20+
import kotlin.test.*
21+
22+
// :todo: This test does not actually test anything because of KT-21970 JS: Support async tests in Mocha and others
23+
// One should watch for errors in the console to see if there were any failures (the test would still pass)
24+
class PromiseTest : TestBase() {
25+
@Test
26+
fun testPromiseResolvedAsDeferred() = promise {
27+
val promise = Promise<String> { resolve, _ ->
28+
resolve("OK")
29+
}
30+
val deferred = promise.asDeferred()
31+
assertEquals("OK", deferred.await())
32+
}
33+
34+
@Test
35+
fun testPromiseRejectedAsDeferred() = promise {
36+
val promise = Promise<String> { _, reject ->
37+
reject(TestException("Rejected"))
38+
}
39+
val deferred = promise.asDeferred()
40+
try {
41+
deferred.await()
42+
expectUnreached()
43+
} catch (e: Throwable) {
44+
assertTrue(e is TestException)
45+
assertEquals("Rejected", e.message)
46+
}
47+
}
48+
49+
@Test
50+
fun testCompletedDeferredAsPromise() = promise {
51+
val deferred = async(coroutineContext, CoroutineStart.UNDISPATCHED) {
52+
// completed right away
53+
"OK"
54+
}
55+
val promise = deferred.asPromise()
56+
assertEquals("OK", promise.await())
57+
}
58+
59+
@Test
60+
fun testWaitForDeferredAsPromise() = promise {
61+
val deferred = async(coroutineContext) {
62+
// will complete later
63+
"OK"
64+
}
65+
val promise = deferred.asPromise()
66+
assertEquals("OK", promise.await()) // await yields main thread to deferred coroutine
67+
}
68+
69+
@Test
70+
fun testCancellableAwaitPromise() = promise {
71+
lateinit var r: (String) -> Unit
72+
val toAwait = Promise<String> { resolve, _ -> r = resolve }
73+
val job = launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
74+
toAwait.await() // suspends
75+
}
76+
job.cancel() // cancel the job
77+
r("fail") // too late, the waiting job was already cancelled
78+
}
79+
80+
@Test
81+
fun testAsPromiseAsDeferred() = promise {
82+
val deferred = async { "OK" }
83+
val promise = deferred.asPromise()
84+
val d2 = promise.asDeferred()
85+
assertTrue(d2 === deferred)
86+
assertEquals("OK", d2.await())
87+
}
88+
89+
private class TestException(message: String) : Exception(message)
90+
}

0 commit comments

Comments
 (0)