Use setTimeout-based dispatcher when process is not available on the … (#1409)
* Use setTimeout-based dispatcher when process is not available on the target runtime
Fixes #1404
diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
index de02723..3390fc1 100644
--- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt
+++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
@@ -9,6 +9,7 @@
private external val navigator: dynamic
private const val UNDEFINED = "undefined"
+internal external val process: dynamic
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
// Check if we are running under ReactNative. We have to use NodeDispatcher under it.
@@ -24,6 +25,8 @@
// Check if we are in the browser and must use window.postMessage to avoid setTimeout throttling
jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED ->
window.asCoroutineDispatcher()
+ // If process is undefined (e.g. in NativeScript, #1404), use SetTimeout-based dispatcher
+ jsTypeOf(process) == UNDEFINED -> SetTimeoutDispatcher
// Fallback to NodeDispatcher when browser environment is not detected
else -> NodeDispatcher
}
diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
index e113777..5a85244 100644
--- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt
+++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
@@ -7,32 +7,69 @@
import kotlinx.coroutines.internal.*
import org.w3c.dom.*
import kotlin.coroutines.*
-import kotlin.js.*
+import kotlin.js.Promise
private const val MAX_DELAY = Int.MAX_VALUE.toLong()
private fun delayToInt(timeMillis: Long): Int =
timeMillis.coerceIn(0, MAX_DELAY).toInt()
-internal object NodeDispatcher : CoroutineDispatcher(), Delay {
- override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block)
+internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay {
+ inner class ScheduledMessageQueue : MessageQueue() {
+ internal val processQueue: dynamic = { process() }
- override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
- val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
- // Actually on cancellation, but clearTimeout is idempotent
- continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler)
+ override fun schedule() {
+ scheduleQueueProcessing()
+ }
+
+ override fun reschedule() {
+ setTimeout(processQueue, 0)
+ }
}
- private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle {
- override fun dispose() { clearTimeout(handle) }
- override fun invoke(cause: Throwable?) { dispose() }
- override fun toString(): String = "ClearTimeout[$handle]"
+ internal val messageQueue = ScheduledMessageQueue()
+
+ abstract fun scheduleQueueProcessing()
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ messageQueue.enqueue(block)
}
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
val handle = setTimeout({ block.run() }, delayToInt(timeMillis))
return ClearTimeout(handle)
}
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
+ val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
+ // Actually on cancellation, but clearTimeout is idempotent
+ continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler)
+ }
+}
+
+internal object NodeDispatcher : SetTimeoutBasedDispatcher() {
+ override fun scheduleQueueProcessing() {
+ process.nextTick(messageQueue.processQueue)
+ }
+}
+
+internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() {
+ override fun scheduleQueueProcessing() {
+ setTimeout(messageQueue.processQueue, 0)
+ }
+}
+
+private class ClearTimeout(private 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 {
@@ -75,17 +112,6 @@
}
}
-private object NodeJsMessageQueue : MessageQueue() {
- override fun schedule() {
- // next tick is even faster than resolve
- process.nextTick({ process() })
- }
-
- override fun reschedule() {
- setTimeout({ process() }, 0)
- }
-}
-
/**
* An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without
* paying the cost of JS callbacks scheduling on every dispatch.
@@ -100,9 +126,8 @@
*/
internal abstract class MessageQueue : ArrayQueue<Runnable>() {
val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
-
private var scheduled = false
-
+
abstract fun schedule()
abstract fun reschedule()
@@ -136,4 +161,3 @@
// 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 external val process: dynamic
diff --git a/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt b/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt
new file mode 100644
index 0000000..7870077
--- /dev/null
+++ b/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines
+
+import kotlin.test.*
+
+class SetTimeoutDispatcherTest : TestBase() {
+ @Test
+ fun testDispatch() = runTest {
+ launch(SetTimeoutDispatcher) {
+ expect(1)
+ launch {
+ expect(3)
+ }
+ expect(2)
+ yield()
+ expect(4)
+ }.join()
+ finish(5)
+ }
+
+ @Test
+ fun testDelay() = runTest {
+ withContext(SetTimeoutDispatcher) {
+ val job = launch(SetTimeoutDispatcher) {
+ expect(2)
+ delay(100)
+ expect(4)
+ }
+ expect(1)
+ yield() // Yield uses microtask, so should be in the same context
+ expect(3)
+ job.join()
+ finish(5)
+ }
+ }
+
+ @Test
+ fun testWithTimeout() = runTest {
+ withContext(SetTimeoutDispatcher) {
+ val result = withTimeoutOrNull(10) {
+ expect(1)
+ delay(100)
+ expectUnreached()
+ 42
+ }
+ assertNull(result)
+ finish(2)
+ }
+ }
+}