Skip to content

Commit 2fc234c

Browse files
authored
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
1 parent a33bf5a commit 2fc234c

File tree

3 files changed

+104
-24
lines changed

3 files changed

+104
-24
lines changed

kotlinx-coroutines-core/js/src/CoroutineContext.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlin.coroutines.*
99

1010
private external val navigator: dynamic
1111
private const val UNDEFINED = "undefined"
12+
internal external val process: dynamic
1213

1314
internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
1415
// Check if we are running under ReactNative. We have to use NodeDispatcher under it.
@@ -24,6 +25,8 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
2425
// Check if we are in the browser and must use window.postMessage to avoid setTimeout throttling
2526
jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED ->
2627
window.asCoroutineDispatcher()
28+
// If process is undefined (e.g. in NativeScript, #1404), use SetTimeout-based dispatcher
29+
jsTypeOf(process) == UNDEFINED -> SetTimeoutDispatcher
2730
// Fallback to NodeDispatcher when browser environment is not detected
2831
else -> NodeDispatcher
2932
}

kotlinx-coroutines-core/js/src/JSDispatcher.kt

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,71 @@ package kotlinx.coroutines
77
import kotlinx.coroutines.internal.*
88
import org.w3c.dom.*
99
import kotlin.coroutines.*
10-
import kotlin.js.*
10+
import kotlin.js.Promise
1111

1212
private const val MAX_DELAY = Int.MAX_VALUE.toLong()
1313

1414
private fun delayToInt(timeMillis: Long): Int =
1515
timeMillis.coerceIn(0, MAX_DELAY).toInt()
1616

17-
internal object NodeDispatcher : CoroutineDispatcher(), Delay {
18-
override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block)
17+
internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay {
18+
inner class ScheduledMessageQueue : MessageQueue() {
19+
internal val processQueue: dynamic = { process() }
20+
21+
override fun schedule() {
22+
scheduleQueueProcessing()
23+
}
24+
25+
override fun reschedule() {
26+
setTimeout(processQueue, 0)
27+
}
28+
}
29+
30+
internal val messageQueue = ScheduledMessageQueue()
31+
32+
abstract fun scheduleQueueProcessing()
33+
34+
override fun dispatch(context: CoroutineContext, block: Runnable) {
35+
messageQueue.enqueue(block)
36+
}
37+
38+
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
39+
val handle = setTimeout({ block.run() }, delayToInt(timeMillis))
40+
return ClearTimeout(handle)
41+
}
1942

2043
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
2144
val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
2245
// Actually on cancellation, but clearTimeout is idempotent
2346
continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler)
2447
}
48+
}
2549

26-
private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle {
27-
override fun dispose() { clearTimeout(handle) }
28-
override fun invoke(cause: Throwable?) { dispose() }
29-
override fun toString(): String = "ClearTimeout[$handle]"
50+
internal object NodeDispatcher : SetTimeoutBasedDispatcher() {
51+
override fun scheduleQueueProcessing() {
52+
process.nextTick(messageQueue.processQueue)
3053
}
54+
}
3155

32-
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
33-
val handle = setTimeout({ block.run() }, delayToInt(timeMillis))
34-
return ClearTimeout(handle)
56+
internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() {
57+
override fun scheduleQueueProcessing() {
58+
setTimeout(messageQueue.processQueue, 0)
3559
}
3660
}
3761

62+
private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle {
63+
64+
override fun dispose() {
65+
clearTimeout(handle)
66+
}
67+
68+
override fun invoke(cause: Throwable?) {
69+
dispose()
70+
}
71+
72+
override fun toString(): String = "ClearTimeout[$handle]"
73+
}
74+
3875
internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay {
3976
private val queue = WindowMessageQueue(window)
4077

@@ -75,17 +112,6 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() {
75112
}
76113
}
77114

78-
private object NodeJsMessageQueue : MessageQueue() {
79-
override fun schedule() {
80-
// next tick is even faster than resolve
81-
process.nextTick({ process() })
82-
}
83-
84-
override fun reschedule() {
85-
setTimeout({ process() }, 0)
86-
}
87-
}
88-
89115
/**
90116
* An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without
91117
* paying the cost of JS callbacks scheduling on every dispatch.
@@ -100,9 +126,8 @@ private object NodeJsMessageQueue : MessageQueue() {
100126
*/
101127
internal abstract class MessageQueue : ArrayQueue<Runnable>() {
102128
val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
103-
104129
private var scheduled = false
105-
130+
106131
abstract fun schedule()
107132

108133
abstract fun reschedule()
@@ -136,4 +161,3 @@ internal abstract class MessageQueue : ArrayQueue<Runnable>() {
136161
// using them via "window" (which only works in browser)
137162
private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int
138163
private external fun clearTimeout(handle: Int = definedExternally)
139-
private external val process: dynamic
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlin.test.*
8+
9+
class SetTimeoutDispatcherTest : TestBase() {
10+
@Test
11+
fun testDispatch() = runTest {
12+
launch(SetTimeoutDispatcher) {
13+
expect(1)
14+
launch {
15+
expect(3)
16+
}
17+
expect(2)
18+
yield()
19+
expect(4)
20+
}.join()
21+
finish(5)
22+
}
23+
24+
@Test
25+
fun testDelay() = runTest {
26+
withContext(SetTimeoutDispatcher) {
27+
val job = launch(SetTimeoutDispatcher) {
28+
expect(2)
29+
delay(100)
30+
expect(4)
31+
}
32+
expect(1)
33+
yield() // Yield uses microtask, so should be in the same context
34+
expect(3)
35+
job.join()
36+
finish(5)
37+
}
38+
}
39+
40+
@Test
41+
fun testWithTimeout() = runTest {
42+
withContext(SetTimeoutDispatcher) {
43+
val result = withTimeoutOrNull(10) {
44+
expect(1)
45+
delay(100)
46+
expectUnreached()
47+
42
48+
}
49+
assertNull(result)
50+
finish(2)
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)