Repository navigation
Move Promise-related functions to the webMain source set - #4563
Conversation
| */ | ||
| @ExperimentalWasmJsInterop | ||
| public fun <T> CoroutineScope.promise( | ||
| @OptIn(ExperimentalWasmJsInterop::class) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Another approach I tried to get to more granular ExperimentalWasmJsInterop opt-ins is
// web
@OptionalExpectation
internal expect annotation class ExperimentalWasmJsInteropWasmJsOnly
// wasmJs
internal actual typealias ExperimentalWasmJsInteropWasmJsOnly =
ExperimentalWasmJsInteropThis causes an error:
[OPT_IN_MARKER_CAN_ONLY_
BE_USED_AS_ANNOTATION_OR_ARGUMENT_IN_OPT_IN] This class can only be used as an annotation or as an argument to '@OptIn'.
| val deferred = async { | ||
| // will complete later | ||
| "OK" | ||
| "OK".toJsReference() |
There was a problem hiding this comment.
@dkhalanskyjb would it work with toJsString()? I would prefer to test conversion into Js$TYPE_NAME instead of toJsReference
There was a problem hiding this comment.
It would. Added a new pair of tests that check that: 8316ce2
JSMonk
left a comment
There was a problem hiding this comment.
I like the solution. The only thing I would like to change is to extend (or change) the test cases with usage of Wasm<->JS types convertors like toJsString, toJsNumber, etc (the onle place I marked, I belive it's the only one)
Fixes #4544 Fixes #4526 On Wasm/JS, before this commit, we had: ```kotlin fun <T> CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Promise<JsAny?> fun <T> Deferred<T>.asPromise(): Promise<JsAny?> fun <T> Promise<JsAny?>.asDeferred(): Deferred<T> suspend fun <T> Promise<JsAny?>.await(): T ``` These signatures are either losing type information (`promise`, `asPromise`) or are type-unsafe (`asDeferred`, `await`). The way an arbitrary `T` is converted into a `JsAny?` is by calling ```kotlin public actual fun <T : Any> T.toJsReference(): JsReference<T> = implementedAsIntrinsic ``` Note the `Any` type bound, it's going to be important. The opposite direction is taken by just an unchecked type cast: ```kotlin value as T ``` The *correct* types for the current implementations of the functions we have are: ```kotlin fun <T> CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T? ): Promise<JsReference<T & Any>?> fun <T> Deferred<T?>.asPromise(): Promise<JsReference<T & Any>?> fun <T> Promise<T: JsAny?>.asDeferred(): Deferred<T> suspend fun <T: JsAny?> Promise<T>.await(): T ``` In principle, since Kotlin/Wasm/JS is experimental, we can break the existing usages if it is necessary. 1. Of course, we don't have `& Any`, which is an issue for this use case. If `block` returns a `null`, then the whole `Promise` resolves to `null`, and if not, it resolves to the proper `JsReference<T>` where `T` is non-`null`. 2. There is a behavior in Kotlin/Wasm/JS that makes the correct types for `await()` and `asDeferred()` incompatible with the current ones: `JsReference<T> as T` works, but does not happen automatically, so where you had `await<String>()` before, now, you will only be able to do `await<JsReference<String>>().get()`. The first issue can be worked around by having two overloads: ```kotlin fun <T: Any> CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T? ): Promise<JsReference<T>?> fun <T: Any> Deferred<T?>.asPromise(): Promise<JsReference<T>?> // These last two are questionable: fun <T: Any> CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Promise<JsReference<T>> fun <T: Any> Deferred<T>.asPromise(): Promise<JsReference<T>> ``` This prevents writing code polymorphic in `T: Any?` that uses these functions, but this limitation may be okay in practice. The second issue may also be solved by introducing extra overloads, but it doesn't look pretty: ```kotlin suspend fun <T: JsAny?> Promise<T>.await(): T // This one is questionable: suspend fun <T: Any> Promise<JsReference<T>>.await(): T ``` These two signatures are also interchangeable with those that Kotlin/JS had before this commit: ```kotlin fun <T> Promise<T: JsAny?>.asDeferred(): Deferred<T> suspend fun <T: JsAny?> Promise<T>.await(): T ``` Since both Kotlin/JS and these functions are stable, we need to preserve their behaviors, which means These, however, are a challenge: ```kotlin fun <T: JsAny?> CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Promise<T> fun <T: JsAny?> Deferred<T>.asPromise(): Promise<T> ``` On Kotlin/JS, `promise` and `asPromise` are polymorphic in `T` over an arbitrary `T`, but Wasm/JS extends the universe of types with Kotlin-specific ones. The ideal solution would be to provide both on Wasm/JS: have `fun promise` in `webMain` that would only work with `JsReference`, and in `wasmJsMain`, also have a `fun promise` that would accept arbitrary types; the same goes for `Deferred.asPromise`. Then, overload resolution could kick in and choose the most specific overload. Unfortunately, Kotlin does not allow this: ```kotlin internal interface A internal fun <T: A> b(block: () -> T) { } internal fun <T> b(block: () -> T) { } internal fun c() { b { 3 } // does not compile } ``` We could mark the general implementation in `webMain` with `LowPriorityInOverloadResolution`, but then, that implementation does not get chosen at all in Kotlin/Wasm/JS code: ```kotlin internal interface A @Suppress("INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution internal fun <T: A> b(block: () -> T): String { return "" } internal fun <T> b(block: () -> T): T { return block() } internal fun c() { val v: Int = b { 3 } // does compile now val s: String = b { object: A {} } // but this doesn't } ``` The same goes for `Deferred.asPromise`: ```kotlin fun <T: Any> Deferred<T>.asPromise(): Promise<JsReference<T>> { TODO() } fun <T: JsAny?> Deferred<T>.asPromise(): Promise<T> { TODO() } val v = async { "OK".toJsReference() } v.asPromise() // fails to compile ``` Therefore, any `promise` or `asPromise` implementations in `webMain` necessarily *preclude* the implementations that works with arbitrary Kotlin types in Kotlin/Wasm/JS. For compatibility with JS, `promise` and `Deferred.asPromise` common to `web` are limited to the JS types, even on Wasm/JS, which can't be fixed by adding extra overloads to Wasm/JS. On the Wasm/JS side, this is a breaking change. It can usually be mitigated by sticking a `JsReference::get` or `Any::toJsReference` to relevant places, but not always. - For example, if third-party code exposes a `Deferred<String>`, the users will be out of luck trying to obtain a `Promise<JsString>` out of that. - More annoyingly, `GlobalScope.promise { }` launched for the side effects will no longer compile, as `Unit` is not `JsAny`. Every such lambda now has to end with some `JsAny` value, for example, `null`. This change was introduced throughout our codebase as well, as we are also affected by this breakage.
213cff3 to
8316ce2
Compare
|
Shall we possibly contribute the overload resolution potential use-case to the compiler team? |
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) | `1.10.1` β `1.11.0` |  |  | | [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://github.com/Kotlin/kotlinx.coroutines) | `1.10.1` β `1.11.0` |  |  | | [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) | `1.10.1` β `1.11.0` |  |  | --- ### Release Notes <details> <summary>Kotlin/kotlinx.coroutines (org.jetbrains.kotlinx:kotlinx-coroutines-test)</summary> ### [`v1.11.0`](https://github.com/Kotlin/kotlinx.coroutines/blob/HEAD/CHANGES.md#Version-1110) [Compare Source](Kotlin/kotlinx.coroutines@1.10.2...1.11.0) ##### Various - Kotlin was updated to 2.2.20 ([#​4545](Kotlin/kotlinx.coroutines#4545)). - Improved the published jar files ([#​3842](Kotlin/kotlinx.coroutines#3842), [#​4599](Kotlin/kotlinx.coroutines#4599)). - Various documentation improvements, including complete rewrites of structured concurrency and error handling-related KDoc ([#​4433](Kotlin/kotlinx.coroutines#4433), [#​4596](Kotlin/kotlinx.coroutines#4596)). ##### Breaking changes and deprecations - Moved `Promise`-related functions from JS and Wasm/JS to the new `web` target. On Wasm/JS, this is a breaking change. Before the change, `Promise` on Wasm/JS could work with arbitrary Kotlin types, but now, only `JsAny` subtypes are accepted ([#​4563](Kotlin/kotlinx.coroutines#4563)). - Changed handling of coroutine exceptions that can't be propagated on JS and Wasm/JS. Before, exceptions were logged, but now, they are reported to the JS runtime ([#​4451](Kotlin/kotlinx.coroutines#4451), [#​4631](Kotlin/kotlinx.coroutines#4631)). - Deprecated using `CoroutineDispatcher` as the coroutine context key; now, `ContinuationInterceptor` has to be used instead ([#​4333](Kotlin/kotlinx.coroutines#4333)). - Advanced the deprecation levels on `kotlinx-coroutines-test` APIs ([#​4604](Kotlin/kotlinx.coroutines#4604)). - Added lint functions that mark passing a `Job` to coroutine builders as deprecated ([#​4435](Kotlin/kotlinx.coroutines#4435)). ##### Bug fixes and improvements - Added a `callsInPlace(EXACTLY_ONCE)` contract to `runBlocking` in code shared between JVM and Native ([#​4368](Kotlin/kotlinx.coroutines#4368)). - Added a `callsInPlace(EXACTLY_ONCE)` contract to `suspendCancellableCoroutine` ([#​4574](Kotlin/kotlinx.coroutines#4574)). - Fixed `flowOn` incorrectly handling `ThreadContextElement` updates ([#​4403](Kotlin/kotlinx.coroutines#4403)). - Fixed exceptions in user-supplied `Thread.UncaughtExceptionHandler` instances causing the internal coroutines machinery to fail ([#​4516](Kotlin/kotlinx.coroutines#4516)). - Fixed `CoroutineDispatcher.asScheduler` in the RxJava integration not cancelling outstanding work when a `Worker` gets cancelled, which led to memory leaks in some scenarios ([#​4615](Kotlin/kotlinx.coroutines#4615)). - Fixed `SharedFlow` entering an invalid state when a subscriber and an emitter are cancelled simultaneously ([#​4583](Kotlin/kotlinx.coroutines#4583)). - Fixed an R8 optimization leading to `shareIn`/`stateIn` coroutines getting garbage-collected ([#​4646](Kotlin/kotlinx.coroutines#4646)). Thanks, [@​solevic](https://github.com/solevic)! ##### Small additions - Added `CompletableDeferred.asDeferred` for obtaining a read-only `Deferred` view ([#​4408](Kotlin/kotlinx.coroutines#4408)). - Added `SharedFlow.asFlow` for obtaining a `Flow` view with hidden hot flow semantics ([#​4530](Kotlin/kotlinx.coroutines#4530)). Thanks, [@​g000sha256](https://github.com/g000sha256)! - Added a `StateFlow.collectLatest` overload returning `Nothing` to assist with finding unreachable code ([#​4454](Kotlin/kotlinx.coroutines#4454)). - Added `ReceiveChannel.consumeTo` for consuming a `ReceiveChannel` into a `MutableCollection` ([#​4520](Kotlin/kotlinx.coroutines#4520)). - Added a `StateFlow<T>.onSubscription` overload returning a `StateFlow<T>`, similar to `SharedFlow<T>.onSubscription` returning `SharedFlow<T>` ([#​4275](Kotlin/kotlinx.coroutines#4275)). Thanks, [@​xit0c](https://github.com/xit0c)! - Added terminal `Flow` operators for collecting a `Flow` to a `Map` ([#​1541](Kotlin/kotlinx.coroutines#1541)). ##### Changelog relative to version 1.11.0 No changes, only the version is increased. ### [`v1.10.2`](https://github.com/Kotlin/kotlinx.coroutines/blob/HEAD/CHANGES.md#Version-1102) [Compare Source](Kotlin/kotlinx.coroutines@1.10.1...1.10.2) - Fixed the `kotlinx-coroutines-debug` JAR file including the `module-info.class` file twice, resulting in failures in various tooling ([#​4314](Kotlin/kotlinx.coroutines#4314)). Thanks, [@​RyuNen344](https://github.com/RyuNen344)! - Fixed `Flow.stateIn` hanging when the scope is cancelled in advance or the flow is empty ([#​4322](Kotlin/kotlinx.coroutines#4322)). Thanks, [@​francescotescari](https://github.com/francescotescari)! - Improved handling of dispatcher failures in `.limitedParallelism` ([#​4330](Kotlin/kotlinx.coroutines#4330)) and during flow collection ([#​4272](Kotlin/kotlinx.coroutines#4272)). - Fixed `runBlocking` failing to run its coroutine to completion in some cases if its JVM thread got interrupted ([#​4399](Kotlin/kotlinx.coroutines#4399)). - Small tweaks, fixes, and documentation improvements. </details> --- ### Configuration π **Schedule**: (UTC) - Branch creation - "before 9am on tuesday" - Automerge - At any time (no schedule defined) π¦ **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. π **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMzMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEzMy4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiLCJyZW5vdmF0ZSJdfQ==--> Reviewed-on: https://code.futo.org/harbor/harbor/pulls/1829
Fixes #4544
Fixes #4526
On Wasm/JS, before this commit, we had:
These signatures are either losing type information (
promise,asPromise) or are type-unsafe (asDeferred,await).The way an arbitrary
Tis converted into aJsAny?is by callingNote the
Anytype bound, it's going to be important.The opposite direction is taken by just an unchecked type cast:
The correct types for the current implementations of the functions we have are:
In principle, since Kotlin/Wasm/JS is experimental, we can break the existing usages if it is necessary.
& Any, which is an issue for this use case. Ifblockreturns anull, then the wholePromiseresolves tonull, and if not, it resolves to the properJsReference<T>whereTis non-null.await()andasDeferred()incompatible with the current ones:JsReference<T> as Tworks, but does not happen automatically, so where you hadawait<String>()before, now, you will only be able to doawait<JsReference<String>>().get().The first issue can be worked around by having two overloads:
This prevents writing code polymorphic in
T: Any?that uses these functions, but this limitation may be okay in practice.The second issue may also be solved by introducing extra overloads, but it doesn't look pretty:
These two signatures are also interchangeable with those that Kotlin/JS had before this commit:
Since both Kotlin/JS and these functions are stable, we need to preserve their behaviors, which means
These, however, are a challenge:
On Kotlin/JS,
promiseandasPromiseare polymorphic inTover an arbitraryT, but Wasm/JS extends the universe of types with Kotlin-specific ones.The ideal solution would be to provide both on Wasm/JS: have
fun promiseinwebMainthat would only work withJsReference, and inwasmJsMain, also have afun promisethat would accept arbitrary types; the same goes forDeferred.asPromise. Then, overload resolution could kick in and choose the most specific overload.Unfortunately, Kotlin does not allow this:
We could mark the general implementation in
webMainwithLowPriorityInOverloadResolution, but then,that implementation does not get chosen at all in Kotlin/Wasm/JS code:
The same goes for
Deferred.asPromise:Therefore, any
promiseorasPromiseimplementations inwebMainnecessarily preclude the implementations that works with arbitrary Kotlin types in Kotlin/Wasm/JS.For compatibility with JS,
promiseandDeferred.asPromisecommon towebare limited to the JS types, even on Wasm/JS, which can't be fixed by adding extra overloads to Wasm/JS.On the Wasm/JS side, this is a breaking change.
It can usually be mitigated by sticking a
JsReference::getorAny::toJsReferenceto relevant places, but not always.Deferred<String>, the users will be out of luck trying to obtain aPromise<JsString>out of that.GlobalScope.promise { }launched for the side effects will no longer compile, asUnitis notJsAny. Every such lambda now has to end with someJsAnyvalue, for example,null. This change was introduced throughout our codebase as well, as we are also affected by this breakage.