@@ -15,6 +15,7 @@ import kotlin.contracts.InvocationKind
15
15
import kotlin.contracts.contract
16
16
import kotlin.coroutines.CoroutineContext
17
17
import kotlin.coroutines.coroutineContext
18
+ import kotlin.js.JsName
18
19
19
20
/* *
20
21
* Stream scope handles all RPC streams that are launched inside it.
@@ -26,33 +27,46 @@ import kotlin.coroutines.coroutineContext
26
27
* Stream scope is a child of the [CoroutineContext] it was created in.
27
28
* Failure of one request will not cancel all streams in the others.
28
29
*/
29
- @InternalRPCApi
30
30
@OptIn(InternalCoroutinesApi ::class )
31
- public class StreamScope (
31
+ public class StreamScope internal constructor (
32
32
parentContext : CoroutineContext ,
33
33
internal val role : Role ,
34
- ) : CoroutineContext.Element, AutoCloseable {
35
- internal companion object Key : CoroutineContext.Key<StreamScope>
34
+ ): AutoCloseable {
35
+ internal class Element (internal val scope : StreamScope ) : CoroutineContext.Element {
36
+ override val key: CoroutineContext .Key <Element > = Key
37
+
38
+ internal companion object Key : CoroutineContext.Key<Element>
39
+ }
36
40
37
- override val key : CoroutineContext . Key < StreamScope > = Key
41
+ internal val contextElement = Element ( this )
38
42
39
43
private val scopeJob = SupervisorJob (parentContext.job)
40
44
41
45
private val requests = ConcurrentHashMap <String , CoroutineScope >()
42
46
47
+ init {
48
+ scopeJob.invokeOnCompletion {
49
+ close()
50
+ }
51
+ }
52
+
53
+ @InternalRPCApi
43
54
public fun onScopeCompletion (handler : (Throwable ? ) -> Unit ) {
44
55
scopeJob.invokeOnCompletion(handler)
45
56
}
46
57
58
+ @InternalRPCApi
47
59
public fun onScopeCompletion (callId : String , handler : (Throwable ? ) -> Unit ) {
48
60
getRequestScope(callId).coroutineContext.job.invokeOnCompletion(onCancelling = true , handler = handler)
49
61
}
50
62
63
+ @InternalRPCApi
51
64
public fun cancelRequestScopeById (callId : String , message : String , cause : Throwable ? ): Job ? {
52
65
return requests.remove(callId)?.apply { cancel(message, cause) }?.coroutineContext?.job
53
66
}
54
67
55
68
// Group stream launches by callId. In case one fails, so do others
69
+ @InternalRPCApi
56
70
public fun launch (callId : String , block : suspend CoroutineScope .() -> Unit ): Job {
57
71
return getRequestScope(callId).launch(block = block)
58
72
}
@@ -86,19 +100,19 @@ public fun CoroutineContext.withServerStreamScope(): CoroutineContext = withStre
86
100
87
101
@OptIn(InternalCoroutinesApi ::class )
88
102
internal fun CoroutineContext.withStreamScope (role : StreamScope .Role ): CoroutineContext {
89
- return this + StreamScope (this , role).apply {
90
- this @withStreamScope.job.invokeOnCompletion(onCancelling = true ) { close() }
103
+ return this + StreamScope (this , role).contextElement. apply {
104
+ this @withStreamScope.job.invokeOnCompletion(onCancelling = true ) { scope. close() }
91
105
}
92
106
}
93
107
94
108
@InternalRPCApi
95
109
public suspend fun streamScopeOrNull (): StreamScope ? {
96
- return currentCoroutineContext()[StreamScope .Key ]
110
+ return currentCoroutineContext()[StreamScope .Element . Key ]?.scope
97
111
}
98
112
99
113
@InternalRPCApi
100
114
public fun streamScopeOrNull (scope : CoroutineScope ): StreamScope ? {
101
- return scope.coroutineContext[StreamScope .Key ]
115
+ return scope.coroutineContext[StreamScope .Element . Key ]?.scope
102
116
}
103
117
104
118
internal fun noStreamScopeError (): Nothing {
@@ -165,22 +179,53 @@ public suspend fun <T> streamScoped(block: suspend CoroutineScope.() -> T): T {
165
179
}
166
180
167
181
val context = currentCoroutineContext()
182
+ .apply {
183
+ checkContextForStreamScope()
184
+ }
185
+
186
+ val streamScope = StreamScope (context, StreamScope .Role .Client )
187
+
188
+ return withContext(streamScope.contextElement) {
189
+ streamScope.use {
190
+ block()
191
+ }
192
+ }
193
+ }
168
194
169
- if (context[StreamScope .Key ] != null ) {
195
+ private fun CoroutineContext.checkContextForStreamScope () {
196
+ if (this [StreamScope .Element ] != null ) {
170
197
error(
171
198
" One of the following caused a failure: \n " +
172
- " - nested 'streamScoped' calls are not allowed.\n " +
173
- " - 'streamScoped' calls are not allowed in server RPC services."
199
+ " - nested 'streamScoped' or `withStreamScope` calls are not allowed.\n " +
200
+ " - 'streamScoped' or `withStreamScope` calls are not allowed in server RPC services."
174
201
)
175
202
}
203
+ }
176
204
177
- val streamScope = StreamScope (context, StreamScope .Role .Client )
205
+ /* *
206
+ * Creates a [StreamScope] entity for manual stream management.
207
+ */
208
+ @JsName(" StreamScope_fun" )
209
+ @ExperimentalRPCApi
210
+ public fun StreamScope (parent : CoroutineContext ): StreamScope {
211
+ parent.checkContextForStreamScope()
178
212
179
- return withContext(streamScope) {
180
- streamScope.use {
181
- block()
182
- }
213
+ return StreamScope (parent, StreamScope .Role .Client )
214
+ }
215
+
216
+ /* *
217
+ * Adds manually managed [StreamScope] to the current context.
218
+ */
219
+ @OptIn(ExperimentalContracts ::class )
220
+ @ExperimentalRPCApi
221
+ public suspend fun <T > withStreamScope (scope : StreamScope , block : suspend CoroutineScope .() -> T ): T {
222
+ contract {
223
+ callsInPlace(block, InvocationKind .EXACTLY_ONCE )
183
224
}
225
+
226
+ currentCoroutineContext().checkContextForStreamScope()
227
+
228
+ return withContext(scope.contextElement, block)
184
229
}
185
230
186
231
/* *
0 commit comments