4
4
5
5
package kotlinx.coroutines.reactive
6
6
7
- import kotlinx.coroutines.CancellationException
8
- import kotlinx.coroutines.Job
9
- import kotlinx.coroutines.suspendCancellableCoroutine
7
+ import kotlinx.coroutines.*
10
8
import org.reactivestreams.Publisher
11
9
import org.reactivestreams.Subscriber
12
10
import org.reactivestreams.Subscription
11
+ import java.lang.IllegalStateException
13
12
import java.util.*
14
13
import kotlin.coroutines.*
15
14
@@ -134,31 +133,61 @@ private suspend fun <T> Publisher<T>.awaitOne(
134
133
mode : Mode ,
135
134
default : T ? = null
136
135
): T = suspendCancellableCoroutine { cont ->
136
+ /* This implementation must obey
137
+ https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md#2-subscriber-code
138
+ The numbers of rules are taken from there. */
137
139
injectCoroutineContext(cont.context).subscribe(object : Subscriber <T > {
138
- private lateinit var subscription: Subscription
140
+ // It is unclear whether 2.13 implies (T: Any), but if so, it seems that we don't break anything by not adhering
141
+ private var subscription: Subscription ? = null
139
142
private var value: T ? = null
140
143
private var seenValue = false
144
+ private var inTerminalState = false
141
145
142
146
override fun onSubscribe (sub : Subscription ) {
147
+ /* * cancelling the new subscription due to rule 2.5, though the publisher would either have to
148
+ * subscribe more than once, which would break 2.12, or leak this [Subscriber]. */
149
+ if (subscription != null ) {
150
+ sub.cancel()
151
+ return
152
+ }
143
153
subscription = sub
144
154
cont.invokeOnCancellation { sub.cancel() }
145
- sub.request(if (mode == Mode .FIRST ) 1 else Long .MAX_VALUE )
155
+ sub.request(if (mode == Mode .FIRST || mode == Mode . FIRST_OR_DEFAULT ) 1 else Long .MAX_VALUE )
146
156
}
147
157
148
158
override fun onNext (t : T ) {
159
+ val sub = subscription.let {
160
+ if (it == null ) {
161
+ /* * Enforce rule 1.9: expect [Subscriber.onSubscribe] before any other signals. */
162
+ handleCoroutineException(cont.context,
163
+ IllegalStateException (" 'onNext' was called before 'onSubscribe'" ))
164
+ return
165
+ } else {
166
+ it
167
+ }
168
+ }
169
+ if (inTerminalState) {
170
+ gotSignalInTerminalStateException(cont.context, " onNext" )
171
+ return
172
+ }
149
173
when (mode) {
150
174
Mode .FIRST , Mode .FIRST_OR_DEFAULT -> {
151
- if (! seenValue) {
152
- seenValue = true
153
- subscription.cancel()
154
- cont.resume(t)
175
+ if (seenValue) {
176
+ moreThanOneValueProvidedException(cont.context, mode)
177
+ return
155
178
}
179
+ seenValue = true
180
+ sub.cancel()
181
+ cont.resume(t)
156
182
}
157
183
Mode .LAST , Mode .SINGLE , Mode .SINGLE_OR_DEFAULT -> {
158
184
if ((mode == Mode .SINGLE || mode == Mode .SINGLE_OR_DEFAULT ) && seenValue) {
159
- subscription.cancel()
160
- if (cont.isActive)
185
+ sub.cancel()
186
+ /* the check for `cont.isActive` is needed in case `sub.cancel() above calls `onComplete` or
187
+ `onError` on its own. */
188
+ if (cont.isActive) {
161
189
cont.resumeWithException(IllegalArgumentException (" More than one onNext value for $mode " ))
190
+ }
162
191
} else {
163
192
value = t
164
193
seenValue = true
@@ -169,23 +198,60 @@ private suspend fun <T> Publisher<T>.awaitOne(
169
198
170
199
@Suppress(" UNCHECKED_CAST" )
171
200
override fun onComplete () {
201
+ if (! tryEnterTerminalState(" onComplete" )) {
202
+ return
203
+ }
172
204
if (seenValue) {
173
- if (cont.isActive) cont.resume(value as T )
205
+ /* the check for `cont.isActive` is needed because, otherwise, if the publisher doesn't acknowledge the
206
+ call to `cancel` for modes `SINGLE*` when more than one value was seen, it may call `onComplete`, and
207
+ here `cont.resume` would fail. */
208
+ if (mode != Mode .FIRST_OR_DEFAULT && mode != Mode .FIRST && cont.isActive) {
209
+ cont.resume(value as T )
210
+ }
174
211
return
175
212
}
176
213
when {
177
214
(mode == Mode .FIRST_OR_DEFAULT || mode == Mode .SINGLE_OR_DEFAULT ) -> {
178
215
cont.resume(default as T )
179
216
}
180
217
cont.isActive -> {
218
+ // the check for `cont.isActive` is just a slight optimization and doesn't affect correctness
181
219
cont.resumeWithException(NoSuchElementException (" No value received via onNext for $mode " ))
182
220
}
183
221
}
184
222
}
185
223
186
224
override fun onError (e : Throwable ) {
187
- cont.resumeWithException(e)
225
+ if (tryEnterTerminalState(" onError" )) {
226
+ cont.resumeWithException(e)
227
+ }
228
+ }
229
+
230
+ /* *
231
+ * Enforce rule 2.4: assume that the [Publisher] is in a terminal state after [onError] or [onComplete].
232
+ */
233
+ private fun tryEnterTerminalState (signalName : String ): Boolean {
234
+ if (inTerminalState) {
235
+ gotSignalInTerminalStateException(cont.context, signalName)
236
+ return false
237
+ }
238
+ inTerminalState = true
239
+ return true
188
240
}
189
241
})
190
242
}
191
243
244
+ /* *
245
+ * Enforce rule 2.4 (detect publishers that don't respect rule 1.7): don't process anything after a terminal
246
+ * state was reached.
247
+ */
248
+ private fun gotSignalInTerminalStateException (context : CoroutineContext , signalName : String ) =
249
+ handleCoroutineException(context,
250
+ IllegalStateException (" '$signalName ' was called after the publisher already signalled being in a terminal state" ))
251
+
252
+ /* *
253
+ * Enforce rule 1.1: it is invalid for a publisher to provide more values than requested.
254
+ */
255
+ private fun moreThanOneValueProvidedException (context : CoroutineContext , mode : Mode ) =
256
+ handleCoroutineException(context,
257
+ IllegalStateException (" Only a single value was requested in '$mode ', but the publisher provided more" ))
0 commit comments