diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index de02723a81..3390fc1b8c 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -9,6 +9,7 @@ import kotlin.coroutines.* 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 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { // 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 e11377718c..5a85244d4a 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -7,34 +7,71 @@ package kotlinx.coroutines 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 schedule() { + scheduleQueueProcessing() + } + + override fun reschedule() { + setTimeout(processQueue, 0) + } + } + + 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) { val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) // Actually on cancellation, but clearTimeout is idempotent continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler) } +} - 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 object NodeDispatcher : SetTimeoutBasedDispatcher() { + override fun scheduleQueueProcessing() { + process.nextTick(messageQueue.processQueue) } +} - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) - return ClearTimeout(handle) +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 { private val queue = WindowMessageQueue(window) @@ -75,17 +112,6 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() { } } -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 @@ private object NodeJsMessageQueue : MessageQueue() { */ internal abstract class MessageQueue : ArrayQueue() { 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 @@ internal abstract class MessageQueue : ArrayQueue() { // 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 0000000000..78700776eb --- /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) + } + } +}