From dce81cbb98ef7aacc8c7075f413a7fc590a5f366 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Fri, 17 Mar 2023 19:51:16 +0100 Subject: [PATCH] [Wasm] Wasm target implementation --- README.md | 2 +- build.gradle | 19 +++ buildSrc/src/main/kotlin/KotlinVersion.kt | 14 -- gradle/compile-js-multiplatform.gradle | 7 + ...mpile-jsAndWasmShared-multiplatform.gradle | 21 +++ gradle/compile-native-multiplatform.gradle | 2 - gradle/compile-wasm-multiplatform.gradle | 26 ++++ gradle/dokka.gradle.kts | 6 + integration-testing/build.gradle | 5 + integration-testing/smokeTest/build.gradle | 20 ++- kotlinx-coroutines-core/build.gradle | 11 +- .../js/src/CoroutineContext.kt | 2 +- .../js/src/JSDispatcher.kt | 144 +++--------------- .../internal/CoroutineExceptionHandlerImpl.kt | 20 +-- .../src/CloseableCoroutineDispatcher.kt | 0 .../src/Dispatchers.kt | 2 + .../{js => jsAndWasmShared}/src/EventLoop.kt | 0 .../{js => jsAndWasmShared}/src/Exceptions.kt | 0 .../{js => jsAndWasmShared}/src/Runnable.kt | 0 .../src/SchedulerTask.kt | 0 .../src/flow/internal/FlowExceptions.kt | 0 .../src/flow/internal/SafeCollector.kt | 0 .../src/internal/Concurrent.kt | 0 .../internal/CoroutineExceptionHandlerImpl.kt | 21 +++ .../src/internal/JSDispatcher.kt | 141 +++++++++++++++++ .../src/internal/LinkedList.kt | 0 .../src/internal/LocalAtomics.kt | 0 .../src/internal/ProbesSupport.kt | 0 .../src/internal/StackTraceRecovery.kt | 0 .../src/internal/Synchronized.kt | 2 +- .../src/internal/SystemProps.kt | 0 .../src/internal/ThreadContext.kt | 0 .../src/internal/ThreadLocal.kt | 2 +- .../test/ImmediateDispatcherTest.kt | 2 +- .../test/MessageQueueTest.kt | 0 .../test/SetTimeoutDispatcherTest.kt | 0 .../test/internal/LinkedListTest.kt | 0 .../native/src/internal/Concurrent.kt | 2 + .../native/src/internal/Synchronized.kt | 2 + .../wasmJs/src/CompletionHandler.kt | 22 +++ .../wasmJs/src/CoroutineContext.kt | 51 +++++++ kotlinx-coroutines-core/wasmJs/src/Debug.kt | 20 +++ .../wasmJs/src/JSDispatcher.kt | 92 +++++++++++ kotlinx-coroutines-core/wasmJs/src/Promise.kt | 85 +++++++++++ .../wasmJs/src/internal/CopyOnWriteList.kt | 73 +++++++++ .../internal/CoroutineExceptionHandlerImpl.kt | 12 ++ .../wasmJs/test/PromiseTest.kt | 90 +++++++++++ .../wasmJs/test/TestBase.kt | 143 +++++++++++++++++ kotlinx-coroutines-test/build.gradle.kts | 13 +- .../wasmJs/src/TestBuilders.kt | 19 +++ .../wasmJs/src/internal/TestMainDispatcher.kt | 13 ++ .../wasmJs/test/Helpers.kt | 19 +++ .../wasmJs/test/PromiseTest.kt | 21 +++ 53 files changed, 981 insertions(+), 165 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/KotlinVersion.kt create mode 100644 gradle/compile-jsAndWasmShared-multiplatform.gradle create mode 100644 gradle/compile-wasm-multiplatform.gradle rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/CloseableCoroutineDispatcher.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/Dispatchers.kt (95%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/EventLoop.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/Exceptions.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/Runnable.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/SchedulerTask.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/flow/internal/FlowExceptions.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/flow/internal/SafeCollector.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/Concurrent.kt (100%) create mode 100644 kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt create mode 100644 kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/LinkedList.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/LocalAtomics.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/ProbesSupport.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/StackTraceRecovery.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/Synchronized.kt (91%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/SystemProps.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/ThreadContext.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/src/internal/ThreadLocal.kt (92%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/test/ImmediateDispatcherTest.kt (90%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/test/MessageQueueTest.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/test/SetTimeoutDispatcherTest.kt (100%) rename kotlinx-coroutines-core/{js => jsAndWasmShared}/test/internal/LinkedListTest.kt (100%) create mode 100644 kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/Debug.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/Promise.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt create mode 100644 kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt create mode 100644 kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt create mode 100644 kotlinx-coroutines-core/wasmJs/test/TestBase.kt create mode 100644 kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt create mode 100644 kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt create mode 100644 kotlinx-coroutines-test/wasmJs/test/Helpers.kt create mode 100644 kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt diff --git a/README.md b/README.md index 90ec53e80b..bb771cdd35 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ See [Contributing Guidelines](CONTRIBUTING.md). [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/promise.html +[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/[js]promise.html [Window.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html diff --git a/build.gradle b/build.gradle index 2b00f602e3..eccd1f0799 100644 --- a/build.gradle +++ b/build.gradle @@ -157,7 +157,12 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != core apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") } + apply from: rootProject.file("gradle/compile-jsAndWasmShared-multiplatform.gradle") + apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") + + apply from: rootProject.file("gradle/compile-wasm-multiplatform.gradle") + kotlin.sourceSets.commonMain.dependencies { api project(":$coreModule") } @@ -368,3 +373,17 @@ if (CacheRedirector.enabled) { nodeJsExtension.nodeDownloadBaseUrl = CacheRedirector.maybeRedirect(nodeJsExtension.nodeDownloadBaseUrl) } } + +// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4 +// check it here: +// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt +extensions.findByType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.class).with { + // canary nodejs that supports recent Wasm GC changes + it.nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2" + it.nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" +} + +// Drop this when node js version become stable +tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask).configureEach { + args.add("--ignore-engines") +} diff --git a/buildSrc/src/main/kotlin/KotlinVersion.kt b/buildSrc/src/main/kotlin/KotlinVersion.kt deleted file mode 100644 index 5ac051ecad..0000000000 --- a/buildSrc/src/main/kotlin/KotlinVersion.kt +++ /dev/null @@ -1,14 +0,0 @@ -@file:JvmName("KotlinVersion") - -fun isKotlinVersionAtLeast(kotlinVersion: String, atLeastMajor: Int, atLeastMinor: Int, atLeastPatch: Int): Boolean { - val (major, minor) = kotlinVersion - .split('.') - .take(2) - .map { it.toInt() } - val patch = kotlinVersion.substringAfterLast('.').substringBefore('-').toInt() - return when { - major > atLeastMajor -> true - major < atLeastMajor -> false - else -> (minor == atLeastMinor && patch >= atLeastPatch) || minor > atLeastMinor - } -} diff --git a/gradle/compile-js-multiplatform.gradle b/gradle/compile-js-multiplatform.gradle index 4200972cea..1935fbf197 100644 --- a/gradle/compile-js-multiplatform.gradle +++ b/gradle/compile-js-multiplatform.gradle @@ -12,6 +12,13 @@ kotlin { } sourceSets { + jsMain { + dependsOn(jsAndWasmSharedMain) + } + jsTest { + dependsOn(jsAndWasmSharedTest) + } + jsTest.dependencies { api "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" } diff --git a/gradle/compile-jsAndWasmShared-multiplatform.gradle b/gradle/compile-jsAndWasmShared-multiplatform.gradle new file mode 100644 index 0000000000..8f489019a8 --- /dev/null +++ b/gradle/compile-jsAndWasmShared-multiplatform.gradle @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +kotlin { + sourceSets { + jsAndWasmSharedMain { + dependsOn(commonMain) + } + jsAndWasmSharedTest { + dependsOn(commonTest) + } + } +} + +// Disable intermediate sourceSet compilation because we do not need js-wasmJs artifact +tasks.configureEach { + if (name == 'compileJsAndWasmSharedMainKotlinMetadata') { + enabled = false + } +} diff --git a/gradle/compile-native-multiplatform.gradle b/gradle/compile-native-multiplatform.gradle index 91a6b62c6b..4f74cf5278 100644 --- a/gradle/compile-native-multiplatform.gradle +++ b/gradle/compile-native-multiplatform.gradle @@ -1,5 +1,3 @@ -import static KotlinVersion.isKotlinVersionAtLeast - /* * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ diff --git a/gradle/compile-wasm-multiplatform.gradle b/gradle/compile-wasm-multiplatform.gradle new file mode 100644 index 0000000000..3692c6d6b8 --- /dev/null +++ b/gradle/compile-wasm-multiplatform.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +kotlin { + wasmJs { + moduleName = project.name + nodejs() + compilations['main']?.dependencies { + api "org.jetbrains.kotlinx:atomicfu-wasm-js:$atomicfu_version" + } + } + + sourceSets { + wasmJsMain { + dependsOn(jsAndWasmSharedMain) + } + wasmJsTest { + dependsOn(jsAndWasmSharedTest) + } + + wasmJsTest.dependencies { + api "org.jetbrains.kotlin:kotlin-test-wasm-js:$kotlin_version" + } + } +} \ No newline at end of file diff --git a/gradle/dokka.gradle.kts b/gradle/dokka.gradle.kts index ba6956aa83..829faad0a0 100644 --- a/gradle/dokka.gradle.kts +++ b/gradle/dokka.gradle.kts @@ -45,6 +45,8 @@ tasks.withType(DokkaTaskPartial::class).configureEach { } } +val kotlin_version: String by project + if (project.name == "kotlinx-coroutines-core") { // Custom configuration for MPP modules tasks.withType(DokkaTaskPartial::class).configureEach { @@ -64,6 +66,10 @@ if (project.name == "kotlinx-coroutines-core") { val jvmMain by getting { makeLinkMapping(project.file("jvm")) } + + val wasmJsMain by getting { + makeLinkMapping(project.file("wasm")) + } } } } diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index 26ee9d99dc..1a231afbdf 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -179,3 +179,8 @@ compileKotlin { jvmTarget = "1.8" } } + +// Drop this when node js version become stable +tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask.class).configureEach { + it.args.add("--ignore-engines") +} diff --git a/integration-testing/smokeTest/build.gradle b/integration-testing/smokeTest/build.gradle index 26cd02b600..ad6a485ed2 100644 --- a/integration-testing/smokeTest/build.gradle +++ b/integration-testing/smokeTest/build.gradle @@ -3,10 +3,10 @@ plugins { } repositories { - // Coroutines from the outer project are published by previous CI buils step - mavenLocal() mavenCentral() maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + // Coroutines from the outer project are published by previous CI buils step + mavenLocal() } kotlin { @@ -14,6 +14,9 @@ kotlin { js(IR) { nodejs() } + wasmJs() { + nodejs() + } sourceSets { commonMain { @@ -34,6 +37,11 @@ kotlin { implementation kotlin('test-js') } } + wasmJsTest { + dependencies { + implementation kotlin('test-wasm-js') + } + } jvmTest { dependencies { implementation kotlin('test') @@ -50,3 +58,11 @@ kotlin { } } +// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4 +// check it here: +// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt +rootProject.extensions.findByType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension.class).with { + // canary nodejs that supports recent Wasm GC changes + it.nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2" + it.nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 7c26e9d280..8609802c00 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -19,8 +19,12 @@ if (rootProject.ext.native_targets_enabled) { apply from: rootProject.file("gradle/compile-native-multiplatform.gradle") } +apply from: rootProject.file("gradle/compile-jsAndWasmShared-multiplatform.gradle") + apply from: rootProject.file("gradle/compile-js-multiplatform.gradle") +apply from: rootProject.file("gradle/compile-wasm-multiplatform.gradle") + apply from: rootProject.file('gradle/dokka.gradle.kts') apply from: rootProject.file('gradle/publish.gradle') /* ========================================================================== @@ -28,9 +32,8 @@ apply from: rootProject.file('gradle/publish.gradle') TARGETS SOURCE SETS ------- ---------------------------------------------- - - js -----------------------------------------------------+ - | + wasmJs \----------> jsAndWasmShared --------------------+ + js / | V jvmCore\ --------> jvm ---------> concurrent -------> common jdk8 / ^ @@ -242,7 +245,7 @@ kotlin.sourceSets { kotlin.sourceSets.configureEach { // Do not apply 'ExperimentalForeignApi' where we have allWarningsAsErrors set - if (it.name in ["jvmMain", "jvmCoreMain", "jsMain", "concurrentMain", "commonMain"]) return + if (it.name in ["jvmMain", "jvmCoreMain", "jsMain", 'wasmJsMain', 'jsAndWasmSharedMain', "concurrentMain", "commonMain"]) return languageSettings { optIn('kotlinx.cinterop.ExperimentalForeignApi') optIn('kotlin.experimental.ExperimentalNativeApi') diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index 232b3e271b..2d80a90730 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -12,7 +12,7 @@ private external val navigator: dynamic private const val UNDEFINED = "undefined" internal external val process: dynamic -internal fun createDefaultDispatcher(): CoroutineDispatcher = when { +internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { // Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source. // It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md // "It's missing a few semantics, especially around origins, as well as MessageEvent source." diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index 8ddb903339..c94985b1c8 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -4,50 +4,36 @@ package kotlinx.coroutines -import kotlinx.coroutines.internal.* import org.w3c.dom.* -import kotlin.coroutines.* import kotlin.js.Promise -private const val MAX_DELAY = Int.MAX_VALUE.toLong() +public actual typealias W3CWindow = Window -private fun delayToInt(timeMillis: Long): Int = - timeMillis.coerceIn(0, MAX_DELAY).toInt() +internal actual fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int = + setTimeout(window, handler, timeout) -internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { - inner class ScheduledMessageQueue : MessageQueue() { - internal val processQueue: dynamic = { process() } +internal actual fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int = + setTimeout(handler, timeout) - override fun schedule() { - scheduleQueueProcessing() - } +internal actual fun w3cClearTimeout(window: W3CWindow, handle: Int) = + window.clearTimeout(handle) - override fun reschedule() { - setTimeout(processQueue, 0) - } - } - - internal val messageQueue = ScheduledMessageQueue() +internal actual fun w3cClearTimeout(handle: Int) = + clearTimeout(handle) - abstract fun scheduleQueueProcessing() +internal actual class ScheduledMessageQueue actual constructor(private val dispatcher: SetTimeoutBasedDispatcher) : MessageQueue() { + internal val processQueue: dynamic = { process() } - override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { - parallelism.checkParallelism() - return this + actual override fun schedule() { + dispatcher.scheduleQueueProcessing() } - override fun dispatch(context: CoroutineContext, block: Runnable) { - messageQueue.enqueue(block) + actual override fun reschedule() { + setTimeout(processQueue, 0) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) - return ClearTimeout(handle) - } - - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) - continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler) + internal actual fun setTimeout(timeout: Int) { + setTimeout(processQueue, timeout) } } @@ -57,48 +43,7 @@ internal object NodeDispatcher : SetTimeoutBasedDispatcher() { } } -internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() { - override fun scheduleQueueProcessing() { - setTimeout(messageQueue.processQueue, 0) - } -} - -private open class ClearTimeout(protected val handle: Int) : CancelHandler(), DisposableHandle { - - override fun dispose() { - clearTimeout(handle) - } - - override fun invoke(cause: Throwable?) { - dispose() - } - - override fun toString(): String = "ClearTimeout[$handle]" -} - -internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay { - private val queue = WindowMessageQueue(window) - - override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block) - - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { - val handle = window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) - continuation.invokeOnCancellation(handler = WindowClearTimeout(handle).asHandler) - } - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis)) - return WindowClearTimeout(handle) - } - - private inner class WindowClearTimeout(handle: Int) : ClearTimeout(handle) { - override fun dispose() { - window.clearTimeout(handle) - } - } -} - -private class WindowMessageQueue(private val window: Window) : MessageQueue() { +internal actual class WindowMessageQueue actual constructor(private val window: W3CWindow) : MessageQueue() { private val messageName = "dispatchCoroutine" init { @@ -110,61 +55,20 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() { }, true) } - override fun schedule() { + actual override fun schedule() { Promise.resolve(Unit).then({ process() }) } - override fun reschedule() { + actual override fun reschedule() { window.postMessage(messageName, "*") } } -/** - * An abstraction over JS scheduling mechanism that leverages micro-batching of dispatched blocks without - * paying the cost of JS callbacks scheduling on every dispatch. - * - * Queue uses two scheduling mechanisms: - * 1) [schedule] is used to schedule the initial processing of the message queue. - * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch - * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop. - * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks. - * - * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size. - */ -internal abstract class MessageQueue : MutableList by ArrayDeque() { - val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages - private var scheduled = false - - abstract fun schedule() - - abstract fun reschedule() - - fun enqueue(element: Runnable) { - add(element) - if (!scheduled) { - scheduled = true - schedule() - } - } - - fun process() { - try { - // limit number of processed messages - repeat(yieldEvery) { - val element = removeFirstOrNull() ?: return@process - element.run() - } - } finally { - if (isEmpty()) { - scheduled = false - } else { - reschedule() - } - } - } -} - // We need to reference global setTimeout and clearTimeout so that it works on Node.JS as opposed to // using them via "window" (which only works in browser) private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int + private external fun clearTimeout(handle: Int = definedExternally) + +private fun setTimeout(window: Window, handler: () -> Unit, timeout: Int): Int = + window.setTimeout(handler, timeout) diff --git a/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt index 675cc4a67a..097f4bb607 100644 --- a/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt +++ b/kotlinx-coroutines-core/js/src/internal/CoroutineExceptionHandlerImpl.kt @@ -1,26 +1,12 @@ /* - * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.internal import kotlinx.coroutines.* -import kotlin.coroutines.* - -private val platformExceptionHandlers_ = mutableSetOf() - -internal actual val platformExceptionHandlers: Collection - get() = platformExceptionHandlers_ - -internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExceptionHandler) { - platformExceptionHandlers_ += callback -} internal actual fun propagateExceptionFinalResort(exception: Throwable) { // log exception - console.error(exception) -} - -internal actual class DiagnosticCoroutineContextException actual constructor(context: CoroutineContext) : - RuntimeException(context.toString()) - + console.error(exception.toString()) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/CloseableCoroutineDispatcher.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/CloseableCoroutineDispatcher.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/CloseableCoroutineDispatcher.kt diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt similarity index 95% rename from kotlinx-coroutines-core/js/src/Dispatchers.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt index 1304c5a9e5..622344b577 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/Dispatchers.kt @@ -6,6 +6,8 @@ package kotlinx.coroutines import kotlin.coroutines.* +internal expect fun createDefaultDispatcher(): CoroutineDispatcher + public actual object Dispatchers { public actual val Default: CoroutineDispatcher = createDefaultDispatcher() public actual val Main: MainCoroutineDispatcher diff --git a/kotlinx-coroutines-core/js/src/EventLoop.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/EventLoop.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/EventLoop.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/EventLoop.kt diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Exceptions.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/Exceptions.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/Exceptions.kt diff --git a/kotlinx-coroutines-core/js/src/Runnable.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/Runnable.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/Runnable.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/Runnable.kt diff --git a/kotlinx-coroutines-core/js/src/SchedulerTask.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/SchedulerTask.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/SchedulerTask.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/SchedulerTask.kt diff --git a/kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/FlowExceptions.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/flow/internal/FlowExceptions.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/FlowExceptions.kt diff --git a/kotlinx-coroutines-core/js/src/flow/internal/SafeCollector.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/SafeCollector.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/flow/internal/SafeCollector.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/flow/internal/SafeCollector.kt diff --git a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Concurrent.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/Concurrent.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/Concurrent.kt diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt new file mode 100644 index 0000000000..0612922edd --- /dev/null +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/CoroutineExceptionHandlerImpl.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +private val platformExceptionHandlers_ = mutableSetOf() + +internal actual val platformExceptionHandlers: Collection + get() = platformExceptionHandlers_ + +internal actual fun ensurePlatformExceptionHandlerLoaded(callback: CoroutineExceptionHandler) { + platformExceptionHandlers_ += callback +} + +internal actual class DiagnosticCoroutineContextException actual constructor(context: CoroutineContext) : + RuntimeException(context.toString()) + diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt new file mode 100644 index 0000000000..b93c0b35f0 --- /dev/null +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +public expect abstract class W3CWindow +internal expect fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int +internal expect fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int +internal expect fun w3cClearTimeout(handle: Int) +internal expect fun w3cClearTimeout(window: W3CWindow, handle: Int) + +internal expect class ScheduledMessageQueue(dispatcher: SetTimeoutBasedDispatcher) : MessageQueue { + override fun schedule() + override fun reschedule() + internal fun setTimeout(timeout: Int) +} + +internal expect class WindowMessageQueue(window: W3CWindow) : MessageQueue { + override fun schedule() + override fun reschedule() +} + +private const val MAX_DELAY = Int.MAX_VALUE.toLong() + +private fun delayToInt(timeMillis: Long): Int = + timeMillis.coerceIn(0, MAX_DELAY).toInt() + +internal abstract class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { + internal val messageQueue = ScheduledMessageQueue(this) + + abstract fun scheduleQueueProcessing() + + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + return this + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + messageQueue.enqueue(block) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + val handle = w3cSetTimeout({ block.run() }, delayToInt(timeMillis)) + return ClearTimeout(handle) + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val handle = w3cSetTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) + continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler) + } +} + +internal class WindowDispatcher(private val window: W3CWindow) : CoroutineDispatcher(), Delay { + private val queue = WindowMessageQueue(window) + + override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block) + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val handle = w3cSetTimeout(window, { with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) + continuation.invokeOnCancellation(handler = WindowClearTimeout(handle).asHandler) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + val handle = w3cSetTimeout(window, block::run, delayToInt(timeMillis)) + return WindowClearTimeout(handle) + } + + private inner class WindowClearTimeout(handle: Int) : ClearTimeout(handle) { + override fun dispose() { + w3cClearTimeout(window, handle) + } + } +} + +internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() { + override fun scheduleQueueProcessing() { + messageQueue.setTimeout(0) + } +} + +private open class ClearTimeout(protected val handle: Int) : CancelHandler(), DisposableHandle { + override fun dispose() { + w3cClearTimeout(handle) + } + + override fun invoke(cause: Throwable?) { + dispose() + } + + override fun toString(): String = "ClearTimeout[$handle]" +} + + +/** + * An abstraction over JS scheduling mechanism that leverages micro-batching of dispatched blocks without + * paying the cost of JS callbacks scheduling on every dispatch. + * + * Queue uses two scheduling mechanisms: + * 1) [schedule] is used to schedule the initial processing of the message queue. + * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch + * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop. + * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks. + * + * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size. + */ +internal abstract class MessageQueue : MutableList by ArrayDeque() { + val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages + private var scheduled = false + + abstract fun schedule() + + abstract fun reschedule() + + fun enqueue(element: Runnable) { + add(element) + if (!scheduled) { + scheduled = true + schedule() + } + } + + fun process() { + try { + // limit number of processed messages + repeat(yieldEvery) { + val element = removeFirstOrNull() ?: return@process + element.run() + } + } finally { + if (isEmpty()) { + scheduled = false + } else { + reschedule() + } + } + } +} diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/LinkedList.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/LinkedList.kt diff --git a/kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/LocalAtomics.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/LocalAtomics.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/LocalAtomics.kt diff --git a/kotlinx-coroutines-core/js/src/internal/ProbesSupport.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ProbesSupport.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/ProbesSupport.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ProbesSupport.kt diff --git a/kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/StackTraceRecovery.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/StackTraceRecovery.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/StackTraceRecovery.kt diff --git a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt similarity index 91% rename from kotlinx-coroutines-core/js/src/internal/Synchronized.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt index 05db52854f..91c422f237 100644 --- a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/Synchronized.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.* * @suppress **This an internal API and should not be used from general code.** */ @InternalCoroutinesApi -public actual typealias SynchronizedObject = Any +public actual open class SynchronizedObject /** * @suppress **This an internal API and should not be used from general code.** diff --git a/kotlinx-coroutines-core/js/src/internal/SystemProps.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/SystemProps.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/SystemProps.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/SystemProps.kt diff --git a/kotlinx-coroutines-core/js/src/internal/ThreadContext.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadContext.kt similarity index 100% rename from kotlinx-coroutines-core/js/src/internal/ThreadContext.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadContext.kt diff --git a/kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt similarity index 92% rename from kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt rename to kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt index c8dd09683f..8800e281e3 100644 --- a/kotlinx-coroutines-core/js/src/internal/ThreadLocal.kt +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/internal/ThreadLocal.kt @@ -11,4 +11,4 @@ internal actual class CommonThreadLocal { actual fun set(value: T) { this.value = value } } -internal actual fun commonThreadLocal(name: Symbol): CommonThreadLocal = CommonThreadLocal() +internal actual fun commonThreadLocal(name: Symbol): CommonThreadLocal = CommonThreadLocal() \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt similarity index 90% rename from kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt rename to kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt index 7ca6a242b2..d2351fdb94 100644 --- a/kotlinx-coroutines-core/js/test/ImmediateDispatcherTest.kt +++ b/kotlinx-coroutines-core/jsAndWasmShared/test/ImmediateDispatcherTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines diff --git a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt similarity index 100% rename from kotlinx-coroutines-core/js/test/MessageQueueTest.kt rename to kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt diff --git a/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt similarity index 100% rename from kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt rename to kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt diff --git a/kotlinx-coroutines-core/js/test/internal/LinkedListTest.kt b/kotlinx-coroutines-core/jsAndWasmShared/test/internal/LinkedListTest.kt similarity index 100% rename from kotlinx-coroutines-core/js/test/internal/LinkedListTest.kt rename to kotlinx-coroutines-core/jsAndWasmShared/test/internal/LinkedListTest.kt diff --git a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt index f46326bcda..0cc78378da 100644 --- a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt @@ -5,11 +5,13 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.* +import kotlinx.cinterop.* import kotlinx.atomicfu.locks.withLock as withLock2 @Suppress("ACTUAL_WITHOUT_EXPECT") internal actual typealias ReentrantLock = kotlinx.atomicfu.locks.SynchronizedObject +@OptIn(UnsafeNumber::class) internal actual inline fun ReentrantLock.withLock(action: () -> T): T = this.withLock2(action) internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet() diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt index 8a8ecfe393..20fc666229 100644 --- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt +++ b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.internal +import kotlinx.cinterop.* import kotlinx.coroutines.* import kotlinx.atomicfu.locks.withLock as withLock2 @@ -16,5 +17,6 @@ public actual typealias SynchronizedObject = kotlinx.atomicfu.locks.Synchronized /** * @suppress **This an internal API and should not be used from general code.** */ +@OptIn(UnsafeNumber::class) @InternalCoroutinesApi public actual inline fun synchronizedImpl(lock: SynchronizedObject, block: () -> T): T = lock.withLock2(block) diff --git a/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt b/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt new file mode 100644 index 0000000000..4835f7968e --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/CompletionHandler.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* + +internal actual abstract class CompletionHandlerBase actual constructor() : LockFreeLinkedListNode(), CompletionHandler { + actual abstract override fun invoke(cause: Throwable?) +} + +internal actual inline val CompletionHandlerBase.asHandler: CompletionHandler get() = this + +internal actual abstract class CancelHandlerBase actual constructor() : CompletionHandler { + actual abstract override fun invoke(cause: Throwable?) +} + +internal actual inline val CancelHandlerBase.asHandler: CompletionHandler get() = this + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun CompletionHandler.invokeIt(cause: Throwable?) = invoke(cause) diff --git a/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt new file mode 100644 index 0000000000..ab37ff88d3 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* +import org.w3c.dom.* +import kotlin.coroutines.* + +internal external interface JsProcess : JsAny { + fun nextTick(handler: () -> Unit) +} + +internal fun tryGetProcess(): JsProcess? = + js("(typeof(process) !== 'undefined' && typeof(process.nextTick) === 'function') ? process : null") + +internal fun tryGetWindow(): Window? = + js("(typeof(window) !== 'undefined' && window != null && typeof(window.addEventListener) === 'function') ? window : null") + +internal actual fun createDefaultDispatcher(): CoroutineDispatcher = + tryGetProcess()?.let(::NodeDispatcher) + ?: tryGetWindow()?.let(::WindowDispatcher) + ?: SetTimeoutDispatcher + +@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI +internal actual val DefaultDelay: Delay + get() = Dispatchers.Default as Delay + +public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { + val combined = coroutineContext + context + return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) + combined + Dispatchers.Default else combined +} + +public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext { + return this + addedContext +} + +// No debugging facilities on Wasm +internal actual inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block() +internal actual inline fun withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block() +internal actual fun Continuation<*>.toDebugString(): String = toString() +internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm + +internal actual class UndispatchedCoroutine actual constructor( + context: CoroutineContext, + uCont: Continuation +) : ScopeCoroutine(context, uCont) { + override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont)) +} diff --git a/kotlinx-coroutines-core/wasmJs/src/Debug.kt b/kotlinx-coroutines-core/wasmJs/src/Debug.kt new file mode 100644 index 0000000000..93e253039f --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/Debug.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +internal actual val DEBUG: Boolean = false + +internal actual val Any.hexAddress: String + get() = this.hashCode().toString() + +internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown" + +internal actual inline fun assert(value: () -> Boolean) {} + +internal external interface Console { + fun error(s: String) +} + +internal external val console: Console \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt b/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt new file mode 100644 index 0000000000..3dfdeff321 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/JSDispatcher.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import org.w3c.dom.Window +import kotlin.js.* + +public actual typealias W3CWindow = Window + +internal actual fun w3cSetTimeout(window: W3CWindow, handler: () -> Unit, timeout: Int): Int = + setTimeout(window, handler, timeout) + +internal actual fun w3cSetTimeout(handler: () -> Unit, timeout: Int): Int = + setTimeout(handler, timeout) + +internal actual fun w3cClearTimeout(window: W3CWindow, handle: Int) = + window.clearTimeout(handle) + +internal actual fun w3cClearTimeout(handle: Int) = + clearTimeout(handle) + +internal actual class ScheduledMessageQueue actual constructor(private val dispatcher: SetTimeoutBasedDispatcher) : MessageQueue() { + internal val processQueue: () -> Unit = ::process + + actual override fun schedule() { + dispatcher.scheduleQueueProcessing() + } + + actual override fun reschedule() { + setTimeout(processQueue, 0) + } + + internal actual fun setTimeout(timeout: Int) { + setTimeout(processQueue, timeout) + } +} + +internal class NodeDispatcher(private val process: JsProcess) : SetTimeoutBasedDispatcher() { + override fun scheduleQueueProcessing() { + process.nextTick(messageQueue.processQueue) + } +} + +@Suppress("UNUSED_PARAMETER") +private fun subscribeToWindowMessages(window: Window, process: () -> Unit): Unit = js("""{ + const handler = (event) => { + if (event.source == window && event.data == 'dispatchCoroutine') { + event.stopPropagation(); + process(); + } + } + window.addEventListener('message', handler, true); +}""") + +@Suppress("UNUSED_PARAMETER") +private fun createRescheduleMessagePoster(window: Window): () -> Unit = + js("() => window.postMessage('dispatchCoroutine', '*')") + +@Suppress("UNUSED_PARAMETER") +private fun createScheduleMessagePoster(process: () -> Unit): () -> Unit = + js("() => Promise.resolve(0).then(process)") + +internal actual class WindowMessageQueue actual constructor(window: W3CWindow) : MessageQueue() { + private val scheduleMessagePoster = createScheduleMessagePoster(::process) + private val rescheduleMessagePoster = createRescheduleMessagePoster(window) + init { + subscribeToWindowMessages(window, ::process) + } + + actual override fun schedule() { + scheduleMessagePoster() + } + + actual override fun reschedule() { + rescheduleMessagePoster() + } +} + +// We need to reference global setTimeout and clearTimeout so that it works on Node.JS as opposed to +// using them via "window" (which only works in browser) +private external fun setTimeout(handler: () -> Unit, timeout: Int): Int + +// d8 doesn't have clearTimeout +@Suppress("UNUSED_PARAMETER") +private fun clearTimeout(handle: Int): Unit = + js("{ if (typeof clearTimeout !== 'undefined') clearTimeout(handle); }") + +@Suppress("UNUSED_PARAMETER") +private fun setTimeout(window: Window, handler: () -> Unit, timeout: Int): Int = + js("window.setTimeout(handler, timeout)") diff --git a/kotlinx-coroutines-core/wasmJs/src/Promise.kt b/kotlinx-coroutines-core/wasmJs/src/Promise.kt new file mode 100644 index 0000000000..f20bee3320 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/Promise.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.coroutines.* +import kotlin.js.* + +@Suppress("UNUSED_PARAMETER") +internal fun promiseSetDeferred(promise: Promise, deferred: JsAny): Unit = + js("promise.deferred = deferred") + +@Suppress("UNUSED_PARAMETER") +internal fun promiseGetDeferred(promise: Promise): JsAny? = js("""{ + console.assert(promise instanceof Promise, "promiseGetDeferred must receive a promise, but got ", promise); + return promise.deferred == null ? null : promise.deferred; +}""") + + +/** + * Starts new coroutine and returns its result as an implementation of [Promise]. + * + * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. + * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden + * with corresponding [context] element. + * + * By default, the coroutine is immediately scheduled for execution. + * Other options can be specified via `start` parameter. See [CoroutineStart] for details. + * + * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param block the coroutine code. + */ +public fun CoroutineScope.promise( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T +): Promise = + async(context, start, block).asPromise() + +/** + * Converts this deferred value to the instance of [Promise]. + */ +public fun Deferred.asPromise(): Promise { + val promise = Promise { resolve, reject -> + invokeOnCompletion { + val e = getCompletionExceptionOrNull() + if (e != null) { + reject(e.toJsReference()) + } else { + resolve(getCompleted()?.toJsReference()) + } + } + } + promiseSetDeferred(promise, this.toJsReference()) + return promise +} + +/** + * Converts this promise value to the instance of [Deferred]. + */ +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE", "UNCHECKED_CAST") +public fun Promise.asDeferred(): Deferred { + val deferred = promiseGetDeferred(this) as? JsReference> + return deferred?.get() ?: GlobalScope.async(start = CoroutineStart.UNDISPATCHED) { await() } +} + +/** + * Awaits for completion of the promise without blocking. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * stops waiting for the promise and immediately resumes with [CancellationException]. + * There is a **prompt cancellation guarantee**. If the job was cancelled while this function was + * suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details. + */ +@Suppress("UNCHECKED_CAST") +public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> + this@await.then( + onFulfilled = { cont.resume(it as T); null }, + onRejected = { cont.resumeWithException(it.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $it")); null } + ) +} diff --git a/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt b/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt new file mode 100644 index 0000000000..5d6f79fbcd --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/internal/CopyOnWriteList.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +@Suppress("UNCHECKED_CAST") +internal class CopyOnWriteList : AbstractMutableList() { + private var array: Array = arrayOfNulls(0) + + override val size: Int + get() = array.size + + override fun add(element: E): Boolean { + val n = size + val update = array.copyOf(n + 1) + update[n] = element + array = update + return true + } + + override fun add(index: Int, element: E) { + rangeCheck(index) + val n = size + val update = arrayOfNulls(n + 1) + array.copyInto(destination = update, endIndex = index) + update[index] = element + array.copyInto(destination = update, destinationOffset = index + 1, startIndex = index, endIndex = n + 1) + array = update + } + + override fun remove(element: E): Boolean { + val index = array.indexOf(element as Any) + if (index == -1) return false + removeAt(index) + return true + } + + override fun removeAt(index: Int): E { + rangeCheck(index) + val n = size + val element = array[index] + val update = arrayOfNulls(n - 1) + array.copyInto(destination = update, endIndex = index) + array.copyInto(destination = update, destinationOffset = index, startIndex = index + 1, endIndex = n) + array = update + return element as E + } + + override fun iterator(): MutableIterator = IteratorImpl(array as Array) + override fun listIterator(): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") + override fun listIterator(index: Int): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") + override fun isEmpty(): Boolean = size == 0 + override fun set(index: Int, element: E): E = throw UnsupportedOperationException("Operation is not supported") + override fun get(index: Int): E = array[rangeCheck(index)] as E + + private class IteratorImpl(private val array: Array) : MutableIterator { + private var current = 0 + + override fun hasNext(): Boolean = current != array.size + + override fun next(): E { + if (!hasNext()) throw NoSuchElementException() + return array[current++] + } + + override fun remove() = throw UnsupportedOperationException("Operation is not supported") + } + + private fun rangeCheck(index: Int) = index.apply { + if (index < 0 || index >= size) throw IndexOutOfBoundsException("index: $index, size: $size") + } +} diff --git a/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt new file mode 100644 index 0000000000..097f4bb607 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/internal/CoroutineExceptionHandlerImpl.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* + +internal actual fun propagateExceptionFinalResort(exception: Throwable) { + // log exception + console.error(exception.toString()) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt new file mode 100644 index 0000000000..214f8294cd --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.js.* +import kotlin.test.* + +class PromiseTest : TestBase() { + @Test + fun testPromiseResolvedAsDeferred() = GlobalScope.promise { + val promise = Promise> { resolve, _ -> + resolve("OK".toJsReference()) + } + val deferred = promise.asDeferred>() + assertEquals("OK", deferred.await().get()) + } + + @Test + fun testPromiseRejectedAsDeferred() = GlobalScope.promise { + lateinit var promiseReject: (JsAny) -> Unit + val promise = Promise { _, reject -> + promiseReject = reject + } + val deferred = promise.asDeferred>() + // reject after converting to deferred to avoid "Unhandled promise rejection" warnings + promiseReject(TestException("Rejected").toJsReference()) + try { + deferred.await() + expectUnreached() + } catch (e: Throwable) { + assertTrue(e is TestException) + assertEquals("Rejected", e.message) + } + } + + @Test + fun testCompletedDeferredAsPromise() = GlobalScope.promise { + val deferred = async(start = CoroutineStart.UNDISPATCHED) { + // completed right away + "OK" + } + val promise = deferred.asPromise() + assertEquals("OK", promise.await()) + } + + @Test + fun testWaitForDeferredAsPromise() = GlobalScope.promise { + val deferred = async { + // will complete later + "OK" + } + val promise = deferred.asPromise() + assertEquals("OK", promise.await()) // await yields main thread to deferred coroutine + } + + @Test + fun testCancellableAwaitPromise() = GlobalScope.promise { + lateinit var r: (JsAny) -> Unit + val toAwait = Promise { resolve, _ -> r = resolve } + val job = launch(start = CoroutineStart.UNDISPATCHED) { + toAwait.await() // suspends + } + job.cancel() // cancel the job + r("fail".toJsString()) // too late, the waiting job was already cancelled + } + + @Test + fun testAsPromiseAsDeferred() = GlobalScope.promise { + val deferred = async { "OK" } + val promise = deferred.asPromise() + val d2 = promise.asDeferred() + 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 + } + } +} diff --git a/kotlinx-coroutines-core/wasmJs/test/TestBase.kt b/kotlinx-coroutines-core/wasmJs/test/TestBase.kt new file mode 100644 index 0000000000..08aedb7414 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/test/TestBase.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.js.* + +public actual val isStressTest: Boolean = false +public actual val stressTestMultiplier: Int = 1 +public actual val stressTestMultiplierSqrt: Int = 1 + +@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") +public actual typealias TestResult = Promise + +public actual val isNative = false + +@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // Counterpart for @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") +public actual open class TestBase actual constructor() { + public actual val isBoundByJsTestTimeout = true + private var actionIndex = 0 + private var finished = false + private var error: Throwable? = null + private var lastTestPromise: Promise? = null + + /** + * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not + * complete successfully even if this exception is consumed somewhere in the test. + */ + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") + public actual fun error(message: Any, cause: Throwable? = null): Nothing { + if (cause != null) println(cause) + val exception = IllegalStateException( + if (cause == null) message.toString() else "$message; caused by $cause") + if (error == null) error = exception + throw exception + } + + private fun printError(message: String, cause: Throwable) { + if (error == null) error = cause + println("$message: $cause") + println(cause) + } + + /** + * Asserts that this invocation is `index`-th in the execution sequence (counting from one). + */ + public actual fun expect(index: Int) { + val wasIndex = ++actionIndex + check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } + } + + /** + * Asserts that this line is never executed. + */ + public actual fun expectUnreached() { + error("Should not be reached") + } + + /** + * Asserts that this it the last action in the test. It must be invoked by any test that used [expect]. + */ + public actual fun finish(index: Int) { + expect(index) + check(!finished) { "Should call 'finish(...)' at most once" } + finished = true + } + + /** + * Asserts that [finish] was invoked + */ + public actual fun ensureFinished() { + require(finished) { "finish(...) should be caller prior to this check" } + } + + public actual fun reset() { + check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" } + actionIndex = 0 + finished = false + } + + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") + public actual fun runTest( + expected: ((Throwable) -> Boolean)? = null, + unhandled: List<(Throwable) -> Boolean> = emptyList(), + block: suspend CoroutineScope.() -> Unit + ): TestResult { + var exCount = 0 + var ex: Throwable? = null + /* + * This is an additional sanity check against `runTest` mis-usage on JS. + * The only way to write an async test on JS is to return Promise from the test function. + * _Just_ launching promise and returning `Unit` won't suffice as the underlying test framework + * won't be able to detect an asynchronous failure in a timely manner. + * We cannot detect such situations, but we can detect the most common erroneous pattern + * in our code base, an attempt to use multiple `runTest` in the same `@Test` method, + * which typically is a premise to the same error: + * ``` + * @Test + * fun incorrectTestForJs() { // <- promise is not returned + * for (parameter in parameters) { + * runTest { + * runTestForParameter(parameter) + * } + * } + * } + * ``` + */ + if (lastTestPromise != null) { + error("Attempt to run multiple asynchronous test within one @Test method") + } + val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e -> + if (e is CancellationException) return@CoroutineExceptionHandler // are ignored + exCount++ + when { + exCount > unhandled.size -> + printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e) + !unhandled[exCount - 1](e) -> + printError("Unhandled exception was unexpected: $e", e) + } + }).catch { jsE -> + val e = jsE.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $jsE") + ex = e + if (expected != null) { + if (!expected(e)) + error("Unexpected exception", e) + } else + throw e + + null + }.finally { + if (ex == null && expected != null) error("Exception was expected but none produced") + if (exCount < unhandled.size) + error("Too few unhandled exceptions $exCount, expected ${unhandled.size}") + error?.let { throw it } + check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" } + } + lastTestPromise = result + return result + } +} + +public actual val isJavaAndWindows: Boolean get() = false \ No newline at end of file diff --git a/kotlinx-coroutines-test/build.gradle.kts b/kotlinx-coroutines-test/build.gradle.kts index 981e04ad64..220c65019c 100644 --- a/kotlinx-coroutines-test/build.gradle.kts +++ b/kotlinx-coroutines-test/build.gradle.kts @@ -26,4 +26,15 @@ kotlin { } } } -} + + wasmJs { + nodejs { + testTask { + filter.apply { + // https://youtrack.jetbrains.com/issue/KT-61888 + excludeTest("TestDispatchersTest", "testMainMocking") + } + } + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt new file mode 100644 index 0000000000..898750b8bd --- /dev/null +++ b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test +import kotlinx.coroutines.* +import kotlin.js.* + +@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") +public actual typealias TestResult = Promise + +internal actual fun systemPropertyImpl(name: String): String? = null + +internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult = + GlobalScope.promise { + testProcedure() + } + +internal actual fun dumpCoroutines() { } \ No newline at end of file diff --git a/kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt new file mode 100644 index 0000000000..4d865f83c0 --- /dev/null +++ b/kotlinx-coroutines-test/wasmJs/src/internal/TestMainDispatcher.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test.internal +import kotlinx.coroutines.* + +@Suppress("INVISIBLE_MEMBER") +internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = + when (val mainDispatcher = Main) { + is TestMainDispatcher -> mainDispatcher + else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) } + } diff --git a/kotlinx-coroutines-test/wasmJs/test/Helpers.kt b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt new file mode 100644 index 0000000000..1a8d63d7a7 --- /dev/null +++ b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlin.test.* + +actual fun testResultChain(block: () -> TestResult, after: (Result) -> TestResult): TestResult = + block().then( + { + after(Result.success(Unit)) + null + }, { + after(Result.failure(it.toThrowableOrNull() ?: Throwable("Unexpected non-Kotlin exception $it"))) + null + }) + +actual typealias NoJs = Ignore \ No newline at end of file diff --git a/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt new file mode 100644 index 0000000000..087db81eb8 --- /dev/null +++ b/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlin.test.* + +class PromiseTest { + @Test + fun testCompletionFromPromise() = runTest { + var promiseEntered = false + val p = promise { + delay(1) + promiseEntered = true + } + delay(2) + p.await() + assertTrue(promiseEntered) + } +} \ No newline at end of file