I see that `CancellableContinuation.tryResume` is ...
# coroutines
e
I see that
CancellableContinuation.tryResume
is marked as internal API. Have you ever had to use it? I have a callback that might fire multiple times, but obviously the continuation must be resumed only once. Does that look like a legitimate use case? Note that I can't get rid of the callback, I still need it for future calls, I just don't want to resume obviously.
j
If you adapt a mutli-fire callback, you should probably use
callbackFlow
instead
☝️ 1
e
I think my case is a bit more specific. I subscribe to an
error
callback during a
suspend fun connect
call, to connect to a socket. The
error
callback can be called instead of
connected
, if connection fails, but also afterwards for any network error, for example.
d
Why are you storing continuations for the callback?
e
I'm not storing continuations. I'm subscribing to an event via a callback inside a
suspendCancellableCoroutine
block.
d
Ah, and that event might be fired more than once, got it.
The documentation for `suspendCancellableCoroutine`:
Copy code
* Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to
 * the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is
 * cancelled or completed while it is suspended.
 *
 * A typical use of this function is to suspend a coroutine while waiting for a result
 * from a single-shot callback API and to return the result to the caller.
 * For multi-shot callback APIs see [callbackFlow][kotlinx.coroutines.flow.callbackFlow].
e
But I can't use a
Flow
, for my use case it's not compatible with the API I want to expose.
d
BTW, unrelated to your question but is this a JVM project?
e
Nope, I'm wrapping an internal JS library.
d
What is the API signature you want to expose?
You might be able to use a lazy for this:
Copy code
fun callbackApi(callback: () -> Unit) {
    repeat(3) {
        callback()
    }
}

suspend fun connect() {
    suspendCancellableCoroutine {
        val once = lazy { it.resume(Unit) }
        callbackApi { once.value }
    }
}
r
Maybe try to split your code differently. If you turn your callback into a Flow you can await a single value in your suspend function. Or alternatively keep a local state value to track if the callback was already fired (ignoring the callback in that case).
Depending on your code you could also introduce a new "connect callback" which is stored as a state value. Once used clear the value and your connect callback won't be called again. Note: you might have to make sure your state value is thread safe
e
Ended up simply checking for the continuation status with
continuation.isActive
. If it isn't active, it means it's been cancelled or completed already (normally or with an exception).
d
Beware the parallelism: if you have several threads that can
resume
, and you have code like this
Copy code
if (isActive) {
  // line X
  resume(Unit)
}
then the coroutine could have been resumed while this thread executes line X.
e
Luckily I'm on JS, so that's not an issue. But if it was, would you suggest maintaining an atomic boolean state, or going with the
tryResume
calls?
d
I'd recommend never using things marked with
InternalCoroutinesApi
unless we have your project in our CI. We can (and sometimes do) remove, rename, edit these API entry points with no concern for backward compatibility.
tryResume
is an implementation detail that may change at any point.
gratitude thank you 1
e
Btw, this is where I first learnt about
try*
functions. https://github.com/Kotlin/kotlinx.coroutines/issues/48 Looks like I'm not alone in this kind of problem.
d
I'm curious: is it guaranteed in your case that the continuation is resumed with exactly the same value (
===
) each time?
e
The thing is the continuation must be resumed exactly once with the error passed in to the callback. All subsequent calls to the callback should not resume again, they should simply handle the error or forward it somewhere else.
But no, the (possible) error will be different each time
🙏 1
To be more clear, I might get a connection error because the TLS cert is invalid. I resume with that error. If I don't get an initial error, a follow up error might be a timeout, but that will be forwarded to another handler.
d
Got it, thanks! Yes, I'd say an atomic boolean is the way to go here.
✔️ 1