Skip to content

Use setTimeout-based dispatcher when process is not available on the … #1409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions kotlinx-coroutines-core/js/src/CoroutineContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand Down
72 changes: 48 additions & 24 deletions kotlinx-coroutines-core/js/src/JSDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>) {
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)

Expand Down Expand Up @@ -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.
Expand All @@ -100,9 +126,8 @@ private object NodeJsMessageQueue : MessageQueue() {
*/
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()
Expand Down Expand Up @@ -136,4 +161,3 @@ internal abstract class MessageQueue : ArrayQueue<Runnable>() {
// 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
53 changes: 53 additions & 0 deletions kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}