Skip to content

Commit d7be4b1

Browse files
committed
[K/JS] Add exportability for Kotlin/JS with conversion to AsyncIterator
With the following commit the `Flow` interface could be consumed on the JavaScript / TypeScript side as follows: ```typescript for await (const value of FLOW_PRODUCER.asAsyncIterable()) { ... } ``` As well as created from from either `AsyncIterable`, `AsyncIterator` or an async generator. With an async iterator the API looks like the following: ```typescript import { Flow } from "coroutines" Flow.fromAsyncGenerator(async function* () { yield await fetch(...) yield "Hello, World" }); ``` ^KT-80733 Fixed
1 parent ffafa81 commit d7be4b1

9 files changed

Lines changed: 1345 additions & 24 deletions

File tree

kotlinx-coroutines-core/common/src/flow/Flow.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ import kotlin.coroutines.*
173173
* These implementations ensure that the context preservation property is not violated, and prevent most
174174
* of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation.
175175
*/
176-
public interface Flow<out T> {
176+
public expect interface Flow<out T> {
177177

178178
/**
179179
* Accepts the given [collector] and [emits][FlowCollector.emit] values into it.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package kotlinx.coroutines.flow
2+
3+
import kotlinx.coroutines.*
4+
import kotlinx.coroutines.flow.internal.*
5+
import kotlin.coroutines.*
6+
7+
/**
8+
* An asynchronous data stream that sequentially emits values and completes normally or with an exception.
9+
*
10+
* _Intermediate operators_ on the flow such as [map], [filter], [take], [zip], etc are functions that are
11+
* applied to the _upstream_ flow or flows and return a _downstream_ flow where further operators can be applied to.
12+
* Intermediate operations do not execute any code in the flow and are not suspending functions themselves.
13+
* They only set up a chain of operations for future execution and quickly return.
14+
* This is known as a _cold flow_ property.
15+
*
16+
* _Terminal operators_ on the flow are either suspending functions such as [collect], [single], [reduce], [toList], etc.
17+
* or [launchIn] operator that starts collection of the flow in the given scope.
18+
* They are applied to the upstream flow and trigger execution of all operations.
19+
* Execution of the flow is also called _collecting the flow_ and is always performed in a suspending manner
20+
* without actual blocking. Terminal operators complete normally or exceptionally depending on successful or failed
21+
* execution of all the flow operations in the upstream. The most basic terminal operator is [collect], for example:
22+
*
23+
* ```
24+
* try {
25+
* flow.collect { value ->
26+
* println("Received $value")
27+
* }
28+
* } catch (e: Exception) {
29+
* println("The flow has thrown an exception: $e")
30+
* }
31+
* ```
32+
*
33+
* By default, flows are _sequential_ and all flow operations are executed sequentially in the same coroutine,
34+
* with an exception for a few operations specifically designed to introduce concurrency into flow
35+
* execution such as [buffer] and [flatMapMerge]. See their documentation for details.
36+
*
37+
* The `Flow` interface does not carry information whether a flow is a _cold_ stream that can be collected repeatedly and
38+
* triggers execution of the same code every time it is collected, or if it is a _hot_ stream that emits different
39+
* values from the same running source on each collection. Usually flows represent _cold_ streams, but
40+
* there is a [SharedFlow] subtype that represents _hot_ streams. In addition to that, any flow can be turned
41+
* into a _hot_ one by the [stateIn] and [shareIn] operators, or by converting the flow into a hot channel
42+
* via the [produceIn] operator.
43+
*
44+
* ### Flow builders
45+
*
46+
* There are the following basic ways to create a flow:
47+
*
48+
* - [flowOf(...)][flowOf] functions to create a flow from a fixed set of values.
49+
* - [asFlow()][asFlow] extension functions on various types to convert them into flows.
50+
* - [flow { ... }][flow] builder function to construct arbitrary flows from
51+
* sequential calls to [emit][FlowCollector.emit] function.
52+
* - [channelFlow { ... }][channelFlow] builder function to construct arbitrary flows from
53+
* potentially concurrent calls to the [send][kotlinx.coroutines.channels.SendChannel.send] function.
54+
* - [MutableStateFlow] and [MutableSharedFlow] define the corresponding constructor functions to create
55+
* a _hot_ flow that can be directly updated.
56+
*
57+
* ### Flow constraints
58+
*
59+
* All implementations of the `Flow` interface must adhere to two key properties described in detail below:
60+
*
61+
* - Context preservation.
62+
* - Exception transparency.
63+
*
64+
* These properties ensure the ability to perform local reasoning about the code with flows and modularize the code
65+
* in such a way that upstream flow emitters can be developed separately from downstream flow collectors.
66+
* A user of a flow does not need to be aware of implementation details of the upstream flows it uses.
67+
*
68+
* ### Context preservation
69+
*
70+
* The flow has a context preservation property: it encapsulates its own execution context and never propagates or leaks
71+
* it downstream, thus making reasoning about the execution context of particular transformations or terminal
72+
* operations trivial.
73+
*
74+
* There is only one way to change the context of a flow: the [flowOn][Flow.flowOn] operator
75+
* that changes the upstream context ("everything above the `flowOn` operator").
76+
* For additional information refer to its documentation.
77+
*
78+
* This reasoning can be demonstrated in practice:
79+
*
80+
* ```
81+
* val flowA = flowOf(1, 2, 3)
82+
* .map { it + 1 } // Will be executed in ctxA
83+
* .flowOn(ctxA) // Changes the upstream context: flowOf and map
84+
*
85+
* // Now we have a context-preserving flow: it is executed somewhere but this information is encapsulated in the flow itself
86+
*
87+
* val filtered = flowA // ctxA is encapsulated in flowA
88+
* .filter { it == 3 } // Pure operator without a context yet
89+
*
90+
* withContext(Dispatchers.Main) {
91+
* // All non-encapsulated operators will be executed in Main: filter and single
92+
* val result = filtered.single()
93+
* myUi.text = result
94+
* }
95+
* ```
96+
*
97+
* From the implementation point of view, it means that all flow implementations should
98+
* only emit from the same coroutine context.
99+
* This constraint is efficiently enforced by the default [flow] builder.
100+
* The [flow] builder should be used if the flow implementation does not start any coroutines.
101+
* Its implementation prevents most of the development mistakes:
102+
*
103+
* ```
104+
* val myFlow = flow {
105+
* // GlobalScope.launch { // is prohibited
106+
* // launch(Dispatchers.IO) { // is prohibited
107+
* // withContext(CoroutineName("myFlow")) { // is prohibited
108+
* emit(1) // OK
109+
* coroutineScope {
110+
* emit(2) // OK -- still the same coroutine
111+
* }
112+
* }
113+
* ```
114+
*
115+
* Use [channelFlow] if the collection and emission of a flow are to be separated into multiple coroutines.
116+
* It encapsulates all the context preservation work and allows you to focus on your
117+
* domain-specific problem, rather than invariant implementation details.
118+
* It is possible to use any combination of coroutine builders from within [channelFlow].
119+
*
120+
* If you are looking for performance and are sure that no concurrent emits and context jumps will happen,
121+
* the [flow] builder can be used alongside a [coroutineScope] or [supervisorScope] instead:
122+
* - Scoped primitive should be used to provide a [CoroutineScope].
123+
* - Changing the context of emission is prohibited, no matter whether it is `withContext(ctx)` or
124+
* a builder argument (e.g. `launch(ctx)`).
125+
* - Collecting another flow from a separate context is allowed, but it has the same effect as
126+
* applying the [flowOn] operator to that flow, which is more efficient.
127+
*
128+
* ### Exception transparency
129+
*
130+
* When `emit` or `emitAll` throws, the Flow implementations must immediately stop emitting new values and finish with an exception.
131+
* For diagnostics or application-specific purposes, the exception may be different from the one thrown by the emit operation,
132+
* suppressing the original exception as discussed below.
133+
* If there is a need to emit values after the downstream failed, please use the [catch][Flow.catch] operator.
134+
*
135+
* The [catch][Flow.catch] operator only catches upstream exceptions, but passes
136+
* all downstream exceptions. Similarly, terminal operators like [collect][Flow.collect]
137+
* throw any unhandled exceptions that occur in their code or in upstream flows, for example:
138+
*
139+
* ```
140+
* flow { emitData() }
141+
* .map { computeOne(it) }
142+
* .catch { ... } // catches exceptions in emitData and computeOne
143+
* .map { computeTwo(it) }
144+
* .collect { process(it) } // throws exceptions from process and computeTwo
145+
* ```
146+
* The same reasoning can be applied to the [onCompletion] operator that is a declarative replacement for the `finally` block.
147+
*
148+
* All exception-handling Flow operators follow the principle of exception suppression:
149+
*
150+
* If the upstream flow throws an exception during its completion when the downstream exception has been thrown,
151+
* the downstream exception becomes superseded and suppressed by the upstream exception, being a semantic
152+
* equivalent of throwing from `finally` block. However, this doesn't affect the operation of the exception-handling operators,
153+
* which consider the downstream exception to be the root cause and behave as if the upstream didn't throw anything.
154+
*
155+
* Failure to adhere to the exception transparency requirement can lead to strange behaviors which make
156+
* it hard to reason about the code because an exception in the `collect { ... }` could be somehow "caught"
157+
* by an upstream flow, limiting the ability of local reasoning about the code.
158+
*
159+
* Flow machinery enforces exception transparency at runtime and throws [IllegalStateException] on any attempt to emit a value,
160+
* if an exception has been thrown on previous attempt.
161+
*
162+
* ### Reactive streams
163+
*
164+
* Flow is [Reactive Streams](http://www.reactive-streams.org/) compliant, you can safely interop it with
165+
* reactive streams using [Flow.asPublisher] and [Publisher.asFlow] from `kotlinx-coroutines-reactive` module.
166+
*
167+
* ### Not stable for inheritance
168+
*
169+
* **The `Flow` interface is not stable for inheritance in 3rd party libraries**, as new methods
170+
* might be added to this interface in the future, but is stable for use.
171+
*
172+
* Use the `flow { ... }` builder function to create an implementation, or extend [AbstractFlow].
173+
* These implementations ensure that the context preservation property is not violated, and prevent most
174+
* of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation.
175+
*/
176+
public actual interface Flow<out T> {
177+
178+
/**
179+
* Accepts the given [collector] and [emits][FlowCollector.emit] values into it.
180+
*
181+
* This method can be used along with SAM-conversion of [FlowCollector]:
182+
* ```
183+
* myFlow.collect { value -> println("Collected $value") }
184+
* ```
185+
*
186+
* ### Method inheritance
187+
*
188+
* To ensure the context preservation property, it is not recommended implementing this method directly.
189+
* Instead, [AbstractFlow] can be used as the base type to properly ensure flow's properties.
190+
*
191+
* All default flow implementations ensure context preservation and exception transparency properties on a best-effort basis
192+
* and throw [IllegalStateException] if a violation was detected.
193+
*/
194+
public actual suspend fun collect(collector: FlowCollector<T>)
195+
}

kotlinx-coroutines-core/js/src/channels/Channel.kt

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
@file:OptIn(ExperimentalJsExport::class, ExperimentalStdlibApi::class)
1+
@file:OptIn(ExperimentalJsExport::class)
22
@file:Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_INTERFACE")
33
package kotlinx.coroutines.channels
44

55
import kotlinx.coroutines.*
6+
import kotlinx.coroutines.internal.JsAsyncIterable
67
import kotlinx.coroutines.internal.recoverStackTrace
78
import kotlinx.coroutines.selects.*
8-
import kotlinx.js.JsPlainObject
99
import kotlin.internal.*
1010
import kotlin.js.Promise
11+
import kotlinx.coroutines.internal.JsAsyncIterator
12+
import kotlinx.coroutines.internal.JsIteratorResult
1113
import kotlin.coroutines.EmptyCoroutineContext
1214

1315
@JsImplicitExport(couldBeConvertedToExplicitExport = true)
@@ -136,24 +138,3 @@ public actual interface ReceiveChannel<out E> : JsAsyncIterable<E> {
136138
) // Warning since 1.3.0, error in 1.5.0, will be hidden or removed in 1.7.0
137139
public actual val onReceiveOrNull: SelectClause1<E?> get() = (this as BufferedChannel<E>).onReceiveOrNull
138140
}
139-
140-
@JsName("AsyncIterable")
141-
internal external interface JsAsyncIterable<out T> {
142-
@JsSymbol("asyncIterator")
143-
public fun asyncIterator(): JsAsyncIterator<T>
144-
}
145-
146-
@JsPlainObject
147-
@JsName("AsyncIterator")
148-
internal external interface JsAsyncIterator<out T> {
149-
public val next: () -> Promise<JsIteratorResult<T>>
150-
public val `return`: () -> Promise<JsIteratorResult<T>>
151-
public val `throw`: (value: Any?) -> Promise<JsIteratorResult<T>>
152-
}
153-
154-
@JsPlainObject
155-
@JsName("IteratorResult")
156-
internal external interface JsIteratorResult<out T> {
157-
public val value: T?
158-
public val done: Boolean
159-
}

0 commit comments

Comments
 (0)