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)
+        }
+    }
+}