Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ suspend fun main() = coroutineScope {
* [Dispatchers.IO] dispatcher for blocking coroutines;
* [Executor.asCoroutineDispatcher][asCoroutineDispatcher] extension, custom thread pools, and more;
* Integrations with `CompletableFuture` and JVM-specific extensions.
* [core/web](kotlinx-coroutines-core/web/) — additional core features available on Kotlin/JS and Kotlin/Wasm/JS:
* Integration with `Promise` via `Promise.await` and [promise] builder.
* [core/js](kotlinx-coroutines-core/js/) — additional core features available on Kotlin/JS:
* Integration with `Promise` via [Promise.await] and [promise] builder;
* Integration with `Window` via [Window.asCoroutineDispatcher], etc.
* [test](kotlinx-coroutines-test/README.md) — test utilities for coroutines:
* [Dispatchers.setMain] to override [Dispatchers.Main] in tests;
Expand Down Expand Up @@ -213,8 +214,7 @@ See [Contributing Guidelines](CONTRIBUTING.md).
[CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html
[Dispatchers.IO]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-i-o.html
[asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html
[Promise.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await.html
[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/[js]promise.html
[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html
[Window.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html

<!--- INDEX kotlinx.coroutines.flow -->
Expand Down
8 changes: 4 additions & 4 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1262,16 +1262,16 @@ final suspend fun (org.w3c.dom/Window).kotlinx.coroutines/awaitAnimationFrame():
final suspend fun <#A: kotlin/Any?> (kotlin.js/Promise<#A>).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise<0:0>(){0§<kotlin.Any?>}[0]

// Targets: [wasmJs]
final fun <#A: kotlin/Any?> (kotlin.js/Promise<kotlin.js/JsAny?>).kotlinx.coroutines/asDeferred(): kotlinx.coroutines/Deferred<#A> // kotlinx.coroutines/asDeferred|asDeferred@kotlin.js.Promise<kotlin.js.JsAny?>(){0§<kotlin.Any?>}[0]
final fun <#A: kotlin.js/JsAny?> (kotlin.js/Promise<#A>).kotlinx.coroutines/asDeferred(): kotlinx.coroutines/Deferred<#A> // kotlinx.coroutines/asDeferred|asDeferred@kotlin.js.Promise<0:0>(){0§<kotlin.js.JsAny?>}[0]

// Targets: [wasmJs]
final fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines/promise(kotlin.coroutines/CoroutineContext = ..., kotlinx.coroutines/CoroutineStart = ..., kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): kotlin.js/Promise<kotlin.js/JsAny?> // kotlinx.coroutines/promise|promise@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.Any?>}[0]
final fun <#A: kotlin.js/JsAny?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines/promise(kotlin.coroutines/CoroutineContext = ..., kotlinx.coroutines/CoroutineStart = ..., kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, #A>): kotlin.js/Promise<#A> // kotlinx.coroutines/promise|promise@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,0:0>){0§<kotlin.js.JsAny?>}[0]

// Targets: [wasmJs]
final fun <#A: kotlin/Any?> (kotlinx.coroutines/Deferred<#A>).kotlinx.coroutines/asPromise(): kotlin.js/Promise<kotlin.js/JsAny?> // kotlinx.coroutines/asPromise|asPromise@kotlinx.coroutines.Deferred<0:0>(){0§<kotlin.Any?>}[0]
final fun <#A: kotlin.js/JsAny?> (kotlinx.coroutines/Deferred<#A>).kotlinx.coroutines/asPromise(): kotlin.js/Promise<#A> // kotlinx.coroutines/asPromise|asPromise@kotlinx.coroutines.Deferred<0:0>(){0§<kotlin.js.JsAny?>}[0]

// Targets: [wasmJs]
final suspend fun <#A: kotlin/Any?> (kotlin.js/Promise<kotlin.js/JsAny?>).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise<kotlin.js.JsAny?>(){0§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin.js/JsAny?> (kotlin.js/Promise<#A>).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise<0:0>(){0§<kotlin.js.JsAny?>}[0]

// Targets: [wasmWasi]
final fun kotlinx.coroutines.internal/runTestCoroutine(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, kotlin/Unit>) // kotlinx.coroutines.internal/runTestCoroutine|runTestCoroutine(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,kotlin.Unit>){}[0]
8 changes: 8 additions & 0 deletions kotlinx-coroutines-core/js/src/Promise.js.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kotlinx.coroutines

@OptIn(ExperimentalWasmJsInterop::class)
internal actual fun JsPromiseError.toThrowable(): Throwable = this as? Throwable ?:
Exception("Promise resolved with a non-Throwable exception '$this' (type ${this::class})")

@OptIn(ExperimentalWasmJsInterop::class)
internal actual fun Throwable.toJsPromiseError(): JsPromiseError = this
67 changes: 0 additions & 67 deletions kotlinx-coroutines-core/js/src/Promise.kt

This file was deleted.

11 changes: 11 additions & 0 deletions kotlinx-coroutines-core/wasmJs/src/Promise.wasm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kotlinx.coroutines

@OptIn(ExperimentalWasmJsInterop::class)
internal actual fun JsPromiseError.toThrowable(): Throwable = try {
unsafeCast<JsReference<Throwable>>().get()
} catch (_: Throwable) {
Exception("Non-Kotlin exception $this of type '${this::class}'")
}

@OptIn(ExperimentalWasmJsInterop::class)
internal actual fun Throwable.toJsPromiseError(): JsPromiseError = toJsReference()
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ internal fun promiseSetDeferred(promise: Promise<JsAny?>, deferred: JsAny): Unit
@Suppress("UNUSED_PARAMETER")
internal fun promiseGetDeferred(promise: Promise<JsAny?>): JsAny? = js("""{
console.assert(promise instanceof Promise, "promiseGetDeferred must receive a promise, but got ", promise);
return promise.deferred == null ? null : promise.deferred;
return promise.deferred == null ? null : promise.deferred;
}""")


/**
* Starts new coroutine and returns its result as an implementation of [Promise].
*
Expand All @@ -31,26 +30,26 @@ internal fun promiseGetDeferred(promise: Promise<JsAny?>): JsAny? = js("""{
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
* @param block the coroutine code.
*/
@ExperimentalWasmJsInterop
public fun <T> CoroutineScope.promise(
@OptIn(ExperimentalWasmJsInterop::class)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me if we want to propagate the opt-in here instead. Are these functions also part of the experimental Wasm/JS interop, or are they fine, even when Promise is not? The description of the annotation https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.js/-experimental-wasm-js-interop/ does not make it clear to me.

The approach taken with Promise (which this PR could also replicate when needed) is to have @ExperimentalWasmJsInterop expect in web, @ExperimentalWasmJsInterop actual in wasmJs, and actual in js (without the annotation).

public fun <T: JsAny?> CoroutineScope.promise(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Promise<JsAny?> =
): Promise<T> =
async(context, start, block).asPromise()

/**
* Converts this deferred value to the instance of [Promise<JsAny?>].
* Converts this deferred value to the instance of [Promise].
*/
@ExperimentalWasmJsInterop
public fun <T> Deferred<T>.asPromise(): Promise<JsAny?> {
val promise = Promise<JsAny?> { resolve, reject ->
@OptIn(ExperimentalWasmJsInterop::class)
public fun <T: JsAny?> Deferred<T>.asPromise(): Promise<T> {
val promise = Promise<T> { resolve, reject ->
invokeOnCompletion {
val e = getCompletionExceptionOrNull()
if (e != null) {
reject(e.toJsReference())
reject(e.toJsPromiseError())
} else {
resolve(getCompleted()?.toJsReference())
resolve(getCompleted())
}
}
}
Expand All @@ -61,9 +60,9 @@ public fun <T> Deferred<T>.asPromise(): Promise<JsAny?> {
/**
* Converts this promise value to the instance of [Deferred].
*/
@ExperimentalWasmJsInterop
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE", "UNCHECKED_CAST")
public fun <T> Promise<JsAny?>.asDeferred(): Deferred<T> {
@OptIn(ExperimentalWasmJsInterop::class)
public fun <T: JsAny?> Promise<T>.asDeferred(): Deferred<T> {
@Suppress("UNCHECKED_CAST", "UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
val deferred = promiseGetDeferred(this) as? JsReference<Deferred<T>>
return deferred?.get() ?: GlobalScope.async(start = CoroutineStart.UNDISPATCHED) { await() }
}
Expand All @@ -76,11 +75,15 @@ public fun <T> Promise<JsAny?>.asDeferred(): Deferred<T> {
* There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled
* while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details.
*/
@ExperimentalWasmJsInterop
@Suppress("UNCHECKED_CAST")
public suspend fun <T> Promise<JsAny?>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
@OptIn(ExperimentalWasmJsInterop::class)
public suspend fun <T: JsAny?> Promise<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
this@await.then(
onFulfilled = { cont.resume(it as T); null },
onRejected = { cont.resumeWithException(it.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $it")); null }
)
onFulfilled = { cont.resume(it); null },
onRejected = { cont.resumeWithException(it.toThrowable()); null })
}

@OptIn(ExperimentalWasmJsInterop::class)
internal expect fun JsPromiseError.toThrowable(): Throwable

@OptIn(ExperimentalWasmJsInterop::class)
internal expect fun Throwable.toJsPromiseError(): JsPromiseError
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,58 @@ import kotlinx.coroutines.testing.*
import kotlin.js.*
import kotlin.test.*

class PromiseTest : TestBase() {
@OptIn(ExperimentalWasmJsInterop::class)
class PromiseTestWeb : TestBase() {
@Test
fun testPromiseResolvedAsDeferred() = GlobalScope.promise {
val promise = Promise<JsReference<String>> { resolve, _ ->
val promise = Promise { resolve, _ ->
resolve("OK".toJsReference())
}
val deferred = promise.asDeferred<JsReference<String>>()
val deferred = promise.asDeferred()
assertEquals("OK", deferred.await().get())
null
}

@Test
fun testPromiseRejectedAsDeferred() = GlobalScope.promise {
lateinit var promiseReject: (JsAny) -> Unit
lateinit var promiseReject: (JsPromiseError) -> Unit
val promise = Promise<JsAny?> { _, reject ->
promiseReject = reject
}
val deferred = promise.asDeferred<JsReference<String>>()
val deferred = promise.asDeferred()
// reject after converting to deferred to avoid "Unhandled promise rejection" warnings
promiseReject(TestException("Rejected").toJsReference())
@Suppress("CAST_NEVER_SUCCEEDS")
promiseReject(TestException("Rejected").toJsReference() as JsPromiseError)
try {
deferred.await()
expectUnreached()
} catch (e: Throwable) {
assertIs<TestException>(e)
assertEquals("Rejected", e.message)
}
null
}

@Test
fun testCompletedDeferredAsPromise() = GlobalScope.promise {
val deferred = async(start = CoroutineStart.UNDISPATCHED) {
// completed right away
"OK"
"OK".toJsReference()
}
val promise = deferred.asPromise()
assertEquals("OK", promise.await())
assertEquals("OK", promise.await().get())
null
}

@Test
fun testWaitForDeferredAsPromise() = GlobalScope.promise {
val deferred = async {
// will complete later
"OK"
"OK".toJsReference()
}
val promise = deferred.asPromise()
assertEquals("OK", promise.await()) // await yields main thread to deferred coroutine
assertEquals("OK", promise.await().get()) // await yields main thread to deferred coroutine
null
}

@Test
Expand All @@ -61,50 +67,44 @@ class PromiseTest : TestBase() {
}
job.cancel() // cancel the job
r("fail".toJsString()) // too late, the waiting job was already cancelled
null
}

@Test
fun testAsPromiseAsDeferred() = GlobalScope.promise {
val deferred = async { "OK" }
val deferred = async { "OK".toJsString() }
val promise = deferred.asPromise()
val d2 = promise.asDeferred<String>()
val d2 = promise.asDeferred<JsString>()
assertSame(d2, deferred)
assertEquals("OK", d2.await())
}

@Test
fun testLeverageTestResult(): TestResult {
// Cannot use expect(..) here
var seq = 0
val result = runTest {
++seq
}
return result.then {
if (seq != 1) error("Unexpected result: $seq")
null
}
assertEquals("OK", d2.await().toString())
null
}

@Test
fun testAwaitPromiseRejectedWithNonKotlinException() = GlobalScope.promise {
lateinit var r: (JsAny) -> Unit
val toAwait = Promise<JsAny?> { _, reject -> r = reject }
val toAwait = jsPromiseRejectedWithString()
val throwable = async(start = CoroutineStart.UNDISPATCHED) {
assertFails { toAwait.await<JsAny?>() }
assertFails { toAwait.await() }
}
r("Rejected".toJsString())
assertIs<JsException>(throwable.await())
val throwableResolved = throwable.await()
assertEquals(true, throwableResolved.message?.contains("Rejected"), "${throwableResolved.message}")
null
}

@Test
fun testAwaitPromiseRejectedWithKotlinException() = GlobalScope.promise {
lateinit var r: (JsAny) -> Unit
lateinit var r: (JsPromiseError) -> Unit
val toAwait = Promise<JsAny?> { _, reject -> r = reject }
val throwable = async(start = CoroutineStart.UNDISPATCHED) {
assertFails { toAwait.await<JsAny?>() }
}
r(RuntimeException("Rejected").toJsReference())
assertIs<RuntimeException>(throwable.await())
@Suppress("CAST_NEVER_SUCCEEDS")
r(RuntimeException("Rejected").toJsReference() as JsPromiseError)
assertIs<Exception>(throwable.await())
assertEquals("Rejected", throwable.await().message)
null
}
}

@OptIn(ExperimentalWasmJsInterop::class)
private fun jsPromiseRejectedWithString(): Promise<JsBigInt> = js("Promise.reject(\"Rejected\")")
4 changes: 2 additions & 2 deletions kotlinx-coroutines-test/api/kotlinx-coroutines-test.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,6 @@ final class kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting { // k
final class kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting : kotlin.js/JsAny { // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting|null[0]
constructor <init>() // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.<init>|<init>(){}[0]

final fun then(kotlin/Function1<kotlin.js/JsAny, kotlin/Unit>): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1<kotlin.js.JsAny,kotlin.Unit>){}[0]
final fun then(kotlin/Function1<kotlin.js/JsAny, kotlin/Unit>, kotlin/Function1<kotlin.js/JsAny, kotlin/Unit>): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1<kotlin.js.JsAny,kotlin.Unit>;kotlin.Function1<kotlin.js.JsAny,kotlin.Unit>){}[0]
final fun then(kotlin/Function1<kotlin.js/JsAny, kotlin.js/JsAny>): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1<kotlin.js.JsAny,kotlin.js.JsAny>){}[0]
final fun then(kotlin/Function1<kotlin.js/JsAny, kotlin.js/JsAny>, kotlin/Function1<kotlin.js/JsAny, kotlin.js/JsAny>): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1<kotlin.js.JsAny,kotlin.js.JsAny>;kotlin.Function1<kotlin.js.JsAny,kotlin.js.JsAny>){}[0]
}
1 change: 1 addition & 0 deletions kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public actual typealias TestResult = JsPromiseInterfaceForTesting
internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult =
GlobalScope.promise {
testProcedure()
Unit.toJsReference()
}.unsafeCast()

internal actual fun dumpCoroutines() { }
Expand Down
Loading