diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api index 5317dbcb48..8b409e3f09 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api @@ -198,6 +198,16 @@ abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.channels/ChannelIter abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.flow/Flow { // kotlinx.coroutines.flow/Flow|null[0] abstract suspend fun collect(kotlinx.coroutines.flow/FlowCollector<#A>) // kotlinx.coroutines.flow/Flow.collect|collect(kotlinx.coroutines.flow.FlowCollector<1:0>){}[0] + + // Targets: [js] + open fun asAsyncIterable(): kotlinx.coroutines.internal/JsAsyncIterable<#A> // kotlinx.coroutines.flow/Flow.asAsyncIterable|asAsyncIterable(){}[0] + + // Targets: [js] + final object Companion { // kotlinx.coroutines.flow/Flow.Companion|null[0] + final fun <#A2: kotlin/Any?> from(kotlin/Function0>): kotlinx.coroutines.flow/Flow<#A2> // kotlinx.coroutines.flow/Flow.Companion.from|from(kotlin.Function0>){0§}[0] + final fun <#A2: kotlin/Any?> from(kotlinx.coroutines.internal/JsAsyncIterable<#A2>): kotlinx.coroutines.flow/Flow<#A2> // kotlinx.coroutines.flow/Flow.Companion.from|from(kotlinx.coroutines.internal.JsAsyncIterable<0:0>){0§}[0] + final fun <#A2: kotlin/Any?> from(kotlinx.coroutines.internal/JsAsyncIterator<#A2>): kotlinx.coroutines.flow/Flow<#A2> // kotlinx.coroutines.flow/Flow.Companion.from|from(kotlinx.coroutines.internal.JsAsyncIterator<0:0>){0§}[0] + } } abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.flow/SharedFlow : kotlinx.coroutines.flow/Flow<#A> { // kotlinx.coroutines.flow/SharedFlow|null[0] @@ -1263,7 +1273,7 @@ final inline fun <#A: kotlin/Any?> kotlinx.coroutines.internal/synchronized(kotl final inline fun <#A: kotlin/Any?> kotlinx.coroutines.internal/synchronizedImpl(kotlinx.coroutines.internal/SynchronizedObject, kotlin/Function0<#A>): #A // kotlinx.coroutines.internal/synchronizedImpl|synchronizedImpl(kotlinx.coroutines.internal.SynchronizedObject;kotlin.Function0<0:0>){0§}[0] // Targets: [js] -abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.channels/ReceiveChannel : kotlinx.coroutines.channels/JsAsyncIterable<#A> { // kotlinx.coroutines.channels/ReceiveChannel|null[0] +abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.channels/ReceiveChannel : kotlinx.coroutines.internal/JsAsyncIterable<#A> { // kotlinx.coroutines.channels/ReceiveChannel|null[0] abstract val isClosedForReceive // kotlinx.coroutines.channels/ReceiveChannel.isClosedForReceive|{}isClosedForReceive[0] abstract fun (): kotlin/Boolean // kotlinx.coroutines.channels/ReceiveChannel.isClosedForReceive.|(){}[0] abstract val isEmpty // kotlinx.coroutines.channels/ReceiveChannel.isEmpty|{}isEmpty[0] @@ -1281,7 +1291,7 @@ abstract interface <#A: out kotlin/Any?> kotlinx.coroutines.channels/ReceiveChan abstract fun tryReceive(): kotlinx.coroutines.channels/ChannelResult<#A> // kotlinx.coroutines.channels/ReceiveChannel.tryReceive|tryReceive(){}[0] abstract suspend fun receive(): #A // kotlinx.coroutines.channels/ReceiveChannel.receive|receive(){}[0] abstract suspend fun receiveCatching(): kotlinx.coroutines.channels/ChannelResult<#A> // kotlinx.coroutines.channels/ReceiveChannel.receiveCatching|receiveCatching(){}[0] - open fun asyncIterator(): kotlinx.coroutines.channels/JsAsyncIterator<#A> // kotlinx.coroutines.channels/ReceiveChannel.asyncIterator|asyncIterator(){}[0] + open fun asyncIterator(): kotlinx.coroutines.internal/JsAsyncIterator<#A> // kotlinx.coroutines.channels/ReceiveChannel.asyncIterator|asyncIterator(){}[0] open fun cancel() // kotlinx.coroutines.channels/ReceiveChannel.cancel|cancel(){}[0] open fun poll(): #A? // kotlinx.coroutines.channels/ReceiveChannel.poll|poll(){}[0] open suspend fun receiveOrNull(): #A? // kotlinx.coroutines.channels/ReceiveChannel.receiveOrNull|receiveOrNull(){}[0] @@ -1303,16 +1313,16 @@ final fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutin final fun <#A: kotlin/Any?> (kotlinx.coroutines/Deferred<#A>).kotlinx.coroutines/asPromise(): kotlin.js/Promise<#A> // kotlinx.coroutines/asPromise|asPromise@kotlinx.coroutines.Deferred<0:0>(){0§}[0] // Targets: [js] -final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_copy_1tks5(noinline kotlinx.coroutines.channels/JsAsyncIterator<#A>, noinline kotlin/Function0>> = ..., noinline kotlin/Function1<#A?, kotlin.js/Promise>> = ..., noinline kotlin/Function1>> = ...): kotlinx.coroutines.channels/JsAsyncIterator<#A> // kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_copy_1tks5|kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_copy_1tks5(kotlinx.coroutines.channels.JsAsyncIterator<0:0>;kotlin.Function0>>;kotlin.Function1<0:0?,kotlin.js.Promise>>;kotlin.Function1>>){0§}[0] +final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_copy_1tks5(noinline kotlinx.coroutines.internal/JsAsyncIterator<#A>, noinline kotlin/Function0>> = ..., noinline kotlin/Function1<#A?, kotlin.js/Promise>> = ..., noinline kotlin/Function1>> = ...): kotlinx.coroutines.internal/JsAsyncIterator<#A> // kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_copy_1tks5|kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_copy_1tks5(kotlinx.coroutines.internal.JsAsyncIterator<0:0>;kotlin.Function0>>;kotlin.Function1<0:0?,kotlin.js.Promise>>;kotlin.Function1>>){0§}[0] // Targets: [js] -final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_invoke_jkqnwo(noinline kotlin/Function0>>, noinline kotlin/Function1<#A?, kotlin.js/Promise>>, noinline kotlin/Function1>>): kotlinx.coroutines.channels/JsAsyncIterator<#A> // kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_invoke_jkqnwo|kotlinx_coroutines_channels_JsAsyncIterator_Companion_btfdib_invoke_jkqnwo(kotlin.Function0>>;kotlin.Function1<0:0?,kotlin.js.Promise>>;kotlin.Function1>>){0§}[0] +final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_invoke_jkqnwo(noinline kotlin/Function0>>, noinline kotlin/Function1<#A?, kotlin.js/Promise>>, noinline kotlin/Function1>>): kotlinx.coroutines.internal/JsAsyncIterator<#A> // kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_invoke_jkqnwo|kotlinx_coroutines_internal_JsAsyncIterator_Companion_9orgqy_invoke_jkqnwo(kotlin.Function0>>;kotlin.Function1<0:0?,kotlin.js.Promise>>;kotlin.Function1>>){0§}[0] // Targets: [js] -final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_copy_1tks5(noinline kotlinx.coroutines.channels/JsIteratorResult<#A>, noinline #A? = ..., noinline kotlin/Boolean = ...): kotlinx.coroutines.channels/JsIteratorResult<#A> // kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_copy_1tks5|kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_copy_1tks5(kotlinx.coroutines.channels.JsIteratorResult<0:0>;0:0?;kotlin.Boolean){0§}[0] +final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_copy_1tks5(noinline kotlinx.coroutines.internal/JsIteratorResult<#A>, noinline #A? = ..., noinline kotlin/Boolean = ...): kotlinx.coroutines.internal/JsIteratorResult<#A> // kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_copy_1tks5|kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_copy_1tks5(kotlinx.coroutines.internal.JsIteratorResult<0:0>;0:0?;kotlin.Boolean){0§}[0] // Targets: [js] -final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_invoke_jkqnwo(noinline #A? = ..., noinline kotlin/Boolean): kotlinx.coroutines.channels/JsIteratorResult<#A> // kotlinx.coroutines.channels/kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_invoke_jkqnwo|kotlinx_coroutines_channels_JsIteratorResult_Companion_6yc9qk_invoke_jkqnwo(0:0?;kotlin.Boolean){0§}[0] +final inline fun <#A: out kotlin/Any?> kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_invoke_jkqnwo(noinline #A? = ..., noinline kotlin/Boolean): kotlinx.coroutines.internal/JsIteratorResult<#A> // kotlinx.coroutines.internal/kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_invoke_jkqnwo|kotlinx_coroutines_internal_JsIteratorResult_Companion_kfjag7_invoke_jkqnwo(0:0?;kotlin.Boolean){0§}[0] // Targets: [js] final suspend fun (org.w3c.dom/Window).kotlinx.coroutines/awaitAnimationFrame(): kotlin/Double // kotlinx.coroutines/awaitAnimationFrame|awaitAnimationFrame@org.w3c.dom.Window(){}[0] diff --git a/kotlinx-coroutines-core/common/src/flow/Flow.kt b/kotlinx-coroutines-core/common/src/flow/Flow.kt index 5735028847..f09e7a85b4 100644 --- a/kotlinx-coroutines-core/common/src/flow/Flow.kt +++ b/kotlinx-coroutines-core/common/src/flow/Flow.kt @@ -173,7 +173,7 @@ import kotlin.coroutines.* * These implementations ensure that the context preservation property is not violated, and prevent most * of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation. */ -public interface Flow { +public expect interface Flow { /** * Accepts the given [collector] and [emits][FlowCollector.emit] values into it. diff --git a/kotlinx-coroutines-core/concurrent/src/flow/Flow.concurrent.kt b/kotlinx-coroutines-core/concurrent/src/flow/Flow.concurrent.kt new file mode 100644 index 0000000000..b70e2647fd --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/src/flow/Flow.concurrent.kt @@ -0,0 +1,5 @@ +package kotlinx.coroutines.flow + +public actual interface Flow { + public actual suspend fun collect(collector: FlowCollector) +} diff --git a/kotlinx-coroutines-core/js/src/channels/Channel.kt b/kotlinx-coroutines-core/js/src/channels/Channel.kt index f2a7945775..731be613bf 100644 --- a/kotlinx-coroutines-core/js/src/channels/Channel.kt +++ b/kotlinx-coroutines-core/js/src/channels/Channel.kt @@ -1,13 +1,15 @@ -@file:OptIn(ExperimentalJsExport::class, ExperimentalStdlibApi::class) +@file:OptIn(ExperimentalJsExport::class) @file:Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_INTERFACE") package kotlinx.coroutines.channels import kotlinx.coroutines.* +import kotlinx.coroutines.internal.JsAsyncIterable import kotlinx.coroutines.internal.recoverStackTrace import kotlinx.coroutines.selects.* -import kotlinx.js.JsPlainObject import kotlin.internal.* import kotlin.js.Promise +import kotlinx.coroutines.internal.JsAsyncIterator +import kotlinx.coroutines.internal.JsIteratorResult @JsImplicitExport(couldBeConvertedToExplicitExport = true) public actual interface ReceiveChannel : JsAsyncIterable { @@ -132,25 +134,3 @@ public actual interface ReceiveChannel : JsAsyncIterable { public actual val onReceiveOrNull: SelectClause1 get() = (this as BufferedChannel).onReceiveOrNull } -@JsName("AsyncIterable") -internal external interface JsAsyncIterable { - @JsSymbol("asyncIterator") - public fun asyncIterator(): JsAsyncIterator -} - -@JsPlainObject -@JsName("AsyncIterator") -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols -internal external interface JsAsyncIterator { - public val next: () -> Promise> - // `return` and `throw` must be able to accept either zero arguments or a single one - public val `return`: (value: @UnsafeVariance T?) -> Promise> - public val `throw`: (value: Any?) -> Promise> -} - -@JsPlainObject -@JsName("IteratorResult") -internal external interface JsIteratorResult { - public val value: T? - public val done: Boolean -} diff --git a/kotlinx-coroutines-core/js/src/flow/Flow.js.kt b/kotlinx-coroutines-core/js/src/flow/Flow.js.kt new file mode 100644 index 0000000000..125c046d61 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/flow/Flow.js.kt @@ -0,0 +1,112 @@ +@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class, ExperimentalStdlibApi::class) +@file:Suppress("INVISIBLE_REFERENCE", "EXPOSED_FUNCTION_RETURN_TYPE", "EXPOSED_PARAMETER_TYPE") +package kotlinx.coroutines.flow + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.JsAsyncIterable +import kotlinx.coroutines.internal.JsAsyncIterator +import kotlinx.coroutines.internal.JsIteratorResult +import kotlinx.coroutines.internal.JsOptionalExport +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.js.Promise + +@JsOptionalExport(couldBeConvertedToExplicitExport = true) +public actual interface Flow { + @JsExport.Ignore + public actual suspend fun collect(collector: FlowCollector) + + /** + * Represents [Flow] as a JavaScript [AsyncIterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols) + * + * This function is a shorthand for: + * `buffer(0).produceIn(GlobalScope)`. + * + * Use it when a [Flow] needs to be exposed to JavaScript APIs that consume + * `AsyncIterable` (for example, via `for await (...)`). + * + * The returned iterable is backed by a coroutine started in [GlobalScope], so its lifecycle + * is not bound to a structured coroutine scope. With `buffer(0)`, elements are relayed with + * rendezvous-style backpressure (producer and consumer synchronize per element). + * + * Kotlin usage: + * ``` + * val flow = flowOf(1, 2, 3) + * val asyncIterable = flow.asAsyncIterable() + * // pass asyncIterable to JS code expecting AsyncIterable + * ``` + * + * JavaScript/TypeScript usage: + * ```javascript + * for await (const value of flow.asAsyncIterable()) { + * console.log(value) + * } + *``` + * + * This API is experimental: behavior and lifecycle semantics may change in future releases. + */ + @ExperimentalCoroutinesApi + public fun asAsyncIterable(): JsAsyncIterable = + buffer(0).produceIn(GlobalScope) + + @JsExport.Ignore + // Important note: it would be much nicer to place those factory functions outside of Flow + // so from both Kotlin and TypeScript side it could be used without importing Flow (like in `flowOf` or `flow`) + // However, the described way of exporting factory functions forces the functions always to be exported (even if people don't use them and don't export Flow), + // and that may cause bundle size problems (at least right now). + // So, until the bundle size problem is solved, we keep those factory functions inside Flow, with possibility to move them outside later. + public companion object { + /** + * Converts a JavaScript AsyncIterable to a Kotlin Flow. + * + * The resulting flow will iterate through all values produced by the async iterable. + * If the flow collection is canceled or fails, the iterator's `return()` method will be called + * to properly clean up the async iterable. + */ + @JsStatic + @ExperimentalCoroutinesApi + public fun from(async: JsAsyncIterable): Flow = + from(async.asyncIterator()) + + /** + * Converts a JavaScript async generator function to a Kotlin Flow. + * + * The generator will be invoked to get an async iterator for collection. + * Cancellation or failure during a collection triggers the iterator's `return()` method + * to ensure proper cleanup. + */ + @JsStatic + @JsName("fromAsyncGenerator") + @ExperimentalCoroutinesApi + public fun from(generator: () -> JsAsyncIterator): Flow = flow { + var completed = false + val iterator = generator() + try { + while (true) { + val result = iterator.next().await() + if (result.done) { + completed = true + break + } + emit(result.value.unsafeCast()) + } + } finally { + if (!completed) { + iterator.asDynamic().`return`().unsafeCast>().await() + } + } + } + + /** + * Converts a JavaScript AsyncIterator to a Kotlin Flow. + * + * The resulting flow emits items produced by the iterator until it reports completion. + * If a collection is canceled or fails, the iterator's `return()` method is called + * to close the iterator. + */ + @JsStatic + @JsName("fromAsyncIterator") + @ExperimentalCoroutinesApi + public fun from(iterator: JsAsyncIterator): Flow = + from { iterator } + } +} diff --git a/kotlinx-coroutines-core/js/src/internal/JsAsyncIteratorProtocol.kt b/kotlinx-coroutines-core/js/src/internal/JsAsyncIteratorProtocol.kt new file mode 100644 index 0000000000..843375cf13 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/internal/JsAsyncIteratorProtocol.kt @@ -0,0 +1,26 @@ +@file:OptIn(ExperimentalStdlibApi::class) +package kotlinx.coroutines.internal + +import kotlinx.js.JsPlainObject +import kotlin.js.Promise + +@JsPlainObject +@JsName("AsyncIterator") +internal external interface JsAsyncIterator { + public val next: () -> Promise> + public val `return`: (value: @UnsafeVariance T?) -> Promise> + public val `throw`: (value: Any?) -> Promise> +} + +@JsPlainObject +@JsName("IteratorResult") +internal external interface JsIteratorResult { + public val value: T? + public val done: Boolean +} + +@JsName("AsyncIterable") +internal external interface JsAsyncIterable { + @JsSymbol("asyncIterator") + public fun asyncIterator(): JsAsyncIterator +} diff --git a/kotlinx-coroutines-core/js/test/ChannelInteropTest.kt b/kotlinx-coroutines-core/js/test/ChannelInteropTest.kt index 49133fe309..69f9646c61 100644 --- a/kotlinx-coroutines-core/js/test/ChannelInteropTest.kt +++ b/kotlinx-coroutines-core/js/test/ChannelInteropTest.kt @@ -1,6 +1,9 @@ package kotlinx.coroutines import kotlinx.coroutines.channels.* +import kotlinx.coroutines.internal.JsAsyncIterator +import kotlinx.coroutines.internal.JsIteratorResult +import kotlinx.coroutines.internal.assertNextStepToBe import kotlinx.coroutines.testing.* import kotlin.js.* import kotlin.test.* @@ -195,14 +198,4 @@ class ChannelInteropTest : TestBase() { assertNextStepToBe(iterator, done = true) assertEquals(2, channel.receive()) } - - private suspend fun assertNextStepToBe( - iterator: JsAsyncIterator, - value: T? = js("undefined"), - done: Boolean = false - ) { - val result = iterator.next().await() - assertEquals(done, result.done) - assertEquals(value, result.value) - } } diff --git a/kotlinx-coroutines-core/js/test/FlowInteropTest.kt b/kotlinx-coroutines-core/js/test/FlowInteropTest.kt new file mode 100644 index 0000000000..5885b437d1 --- /dev/null +++ b/kotlinx-coroutines-core/js/test/FlowInteropTest.kt @@ -0,0 +1,317 @@ +package kotlinx.coroutines + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.internal.JsAsyncIterable +import kotlinx.coroutines.testing.* +import kotlin.js.* +import kotlin.test.* +import kotlinx.coroutines.internal.JsAsyncIterator +import kotlinx.coroutines.internal.JsIteratorResult +import kotlinx.coroutines.internal.assertNextStepToBe + +class FlowInteropTest : TestBase() { + + // ===== Flow to AsyncIterator tests ===== + + @Test + fun testFlowToAsyncIteratorBasic() = runTest { + val flow = flowOf(1, 2, 3) + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 1, done = false) + assertNextStepToBe(iterator, 2, done = false) + assertNextStepToBe(iterator, 3, done = false) + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorEmpty() = runTest { + val flow = emptyFlow() + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorSingle() = runTest { + val flow = flowOf(42) + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 42, done = false) + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorEarlyReturn() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + emit(4) + emit(5) + } + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 1, done = false) + // Call return() to stop iteration early + val returnResult = iterator.asDynamic().`return`().unsafeCast>>().await() + assertEquals(true, returnResult.done) + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorThrow() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + } + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 1, done = false) + // Call throw() to cancel the iterator + val error = js("new Error('test error')") + assertFailsWith { iterator.`throw`(error).await() } + .apply { assertEquals("test error", message) } + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorException() = runTest { + val flow = flow { + emit(1) + emit(2) + throw TestException("test exception") + } + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 1, done = false) + assertNextStepToBe(iterator, 2, done = false) + // Next call should throw the exception + assertFailsWith { iterator.next().await() } + .apply { assertEquals("test exception", message) } + } + + @Test + fun testFlowToAsyncIteratorWithTransformations() = runTest { + val flow = flowOf(1, 2, 3) + .map { it * 2 } + .filter { it > 2 } + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 4, done = false) + assertNextStepToBe(iterator, 6, done = false) + assertNextStepToBe(iterator, done = true) + } + + @Test + fun testFlowToAsyncIteratorCancellationReturnsDone() = runTest { + val flow = flow { + emit(1) + emit(2) + emit(3) + emit(4) + emit(5) + } + val iterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(iterator, 1, done = false) + assertNextStepToBe(iterator, 2, done = false) + val returnResult = iterator.`return`(42).await() + assertEquals(true, returnResult.done) + assertEquals(42, returnResult.value) + assertNextStepToBe(iterator, done = true) + assertNextStepToBe(iterator, done = true) + } + + // ===== AsyncIterator to Flow tests ===== + + @Test + fun testAsyncIteratorToFlowBasic() = runTest { + val asyncIterator = createAsyncIterator(listOf(1, 2, 3)) + val flow = Flow.from(asyncIterator) + val results = mutableListOf() + flow.collect { results.add(it) } + assertEquals(listOf(1, 2, 3), results) + } + + @Test + fun testAsyncIteratorToFlowEmpty() = runTest { + val asyncIterator = createAsyncIterator(emptyList()) + val flow = Flow.from(asyncIterator) + val results = mutableListOf() + flow.collect { results.add(it) } + assertEquals(emptyList(), results) + } + + @Test + fun testAsyncIteratorToFlowSingle() = runTest { + val asyncIterator = createAsyncIterator(listOf(42)) + val flow = Flow.from(asyncIterator) + val results = mutableListOf() + flow.collect { results.add(it) } + assertEquals(listOf(42), results) + } + + @Test + fun testAsyncIteratorToFlowCancellation() = runTest { + var returnCalled = false + var lastIndex = -1 + val asyncIterator = createAsyncIteratorWithCleanup( + listOf(1, 2, 3, 4, 5), + onReturn = { + returnCalled = true + lastIndex = it + } + ) + val flow = Flow.from(asyncIterator) + val results = mutableListOf() + flow.take(2).collect { results.add(it) } + assertEquals(listOf(1, 2), results) + yield() // Allow cleanup to happen + assertTrue(returnCalled, "return() should be called on cancellation") + assertEquals(2, lastIndex, "Not only 1 and 2 were requested by asyncIterator") + } + + @Test + fun testAsyncIteratorToFlowException() = runTest { + val asyncIterator = createAsyncIteratorWithException( + listOf(1, 2), + TestException("iterator error") + ) + val flow = Flow.from(asyncIterator) + val results = mutableListOf() + assertFailsWith { + flow.collect { results.add(it) } + }.apply { + assertEquals("iterator error", message) + } + assertEquals(listOf(1, 2), results) + } + + @Test + fun testAsyncGeneratorToFlow() = runTest { + val generator: () -> JsAsyncIterator = { + createAsyncIterator(listOf(10, 20, 30)) + } + val flow = Flow.from(generator) + val results = mutableListOf() + flow.collect { results.add(it) } + + assertEquals(listOf(10, 20, 30), results) + } + + @Test + fun testAsyncIterableToFlow() = runTest { + val asyncIterable = createAsyncIterable(listOf(5, 10, 15)) + val flow = Flow.from(asyncIterable) + val results = mutableListOf() + flow.collect { results.add(it) } + assertEquals(listOf(5, 10, 15), results) + } + + @Test + fun testAsyncIteratorToFlowWithTransformations() = runTest { + val asyncIterator = createAsyncIterator(listOf(1, 2, 3, 4, 5)) + val flow = Flow.from(asyncIterator) + .map { it * 2 } + .filter { it > 5 } + val results = mutableListOf() + flow.collect { results.add(it) } + assertEquals(listOf(6, 8, 10), results) + } + + // ===== Round-trip tests ===== + @Test + fun testRoundTripFlowToAsyncIterableToFlow() = runTest { + val originalFlow = flowOf(1, 2, 3, 4, 5) + val convertedFlow = Flow.from(originalFlow.asAsyncIterable()) + val results = mutableListOf() + convertedFlow.collect { results.add(it) } + assertEquals(listOf(1, 2, 3, 4, 5), results) + } + + @Test + fun testRoundTripFlowToAsyncIteratorToFlow() = runTest { + val originalFlow = flowOf(1, 2, 3, 4, 5) + val iterator: JsAsyncIterator = originalFlow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + val convertedFlow = Flow.from(iterator) + val results = mutableListOf() + convertedFlow.collect { results.add(it) } + assertEquals(listOf(1, 2, 3, 4, 5), results) + } + + @Test + fun testRoundTripAsyncIteratorToFlowToAsyncIterator() = runTest { + val originalIterator = createAsyncIterator(listOf(10, 20, 30)) + val flow = Flow.from(originalIterator) + val convertedIterator: JsAsyncIterator = flow.asAsyncIterable().asDynamic()[js("Symbol.asyncIterator")]() + assertNextStepToBe(convertedIterator, 10, done = false) + assertNextStepToBe(convertedIterator, 20, done = false) + assertNextStepToBe(convertedIterator, 30, done = false) + assertNextStepToBe(convertedIterator, done = true) + } + + // ===== Helper functions ===== + + private fun createAsyncIterator(values: List): JsAsyncIterator { + var index = 0 + val iterator = js("({})") + iterator.next = fun(): Promise> { + return if (index < values.size) { + val value = values[index++] + Promise.resolve(js("({ value: value, done: false })")) + } else { + Promise.resolve(js("({ value: undefined, done: true })")) + } + } + iterator.`return` = fun(): Promise> { + return Promise.resolve(js("({ value: undefined, done: true })")) + } + iterator.`throw` = fun(error: Throwable): Promise> { + return Promise.reject(error) + } + return iterator + } + + private fun createAsyncIteratorWithCleanup(values: List, onReturn: (Int) -> Unit): JsAsyncIterator { + var index = 0 + val iterator = js("({})") + iterator.next = fun(): Promise> { + return if (index < values.size) { + val value = values[index++] + Promise.resolve(js("({ value: value, done: false })")) + } else { + Promise.resolve(js("({ value: undefined, done: true })")) + } + } + iterator.`return` = fun(): Promise> { + onReturn(index) + return Promise.resolve(js("({ value: undefined, done: true })")) + } + iterator.`throw` = fun(error: Throwable): Promise> { + return Promise.reject(error) + } + return iterator + } + + private fun createAsyncIteratorWithException(values: List, exception: Throwable): JsAsyncIterator { + var index = 0 + val iterator = js("({})") + iterator.next = fun(): Promise> { + return if (index < values.size) { + val value = values[index++] + Promise.resolve(js("({ value: value, done: false })")) + } else { + Promise.reject(exception) + } + } + iterator.`return` = fun(): Promise> { + return Promise.resolve(js("({ value: undefined, done: true })")) + } + iterator.`throw` = fun(error: Throwable): Promise> { + return Promise.reject(error) + } + return iterator + } + + private fun createAsyncIterable(values: List): JsAsyncIterable { + val iterable = js("({})") + iterable[js("Symbol.asyncIterator")] = { + createAsyncIterator(values) + } + return iterable + } +} diff --git a/kotlinx-coroutines-core/js/test/internal/jsIteratorAssertions.kt b/kotlinx-coroutines-core/js/test/internal/jsIteratorAssertions.kt new file mode 100644 index 0000000000..e57ffea026 --- /dev/null +++ b/kotlinx-coroutines-core/js/test/internal/jsIteratorAssertions.kt @@ -0,0 +1,14 @@ +package kotlinx.coroutines.internal + +import kotlinx.coroutines.await +import kotlin.test.assertEquals + +internal suspend fun assertNextStepToBe( + iterator: JsAsyncIterator, + value: T? = js("undefined"), + done: Boolean = false +) { + val result = iterator.next().await() + assertEquals(done, result.done) + assertEquals(value, result.value) +} diff --git a/kotlinx-coroutines-core/wasmJs/src/flow/Flow.wasm.kt b/kotlinx-coroutines-core/wasmJs/src/flow/Flow.wasm.kt new file mode 100644 index 0000000000..b70e2647fd --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/flow/Flow.wasm.kt @@ -0,0 +1,5 @@ +package kotlinx.coroutines.flow + +public actual interface Flow { + public actual suspend fun collect(collector: FlowCollector) +} diff --git a/kotlinx-coroutines-core/wasmWasi/src/flow/Flow.wasm.kt b/kotlinx-coroutines-core/wasmWasi/src/flow/Flow.wasm.kt new file mode 100644 index 0000000000..b70e2647fd --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/flow/Flow.wasm.kt @@ -0,0 +1,5 @@ +package kotlinx.coroutines.flow + +public actual interface Flow { + public actual suspend fun collect(collector: FlowCollector) +}