How can I create a suspended delegated property? I...
# coroutines
r
How can I create a suspended delegated property? I want to access a stored coroutine context element in my property delegate, but that is a suspending val so, and
getValue
operators cannot be suspending due to them taking in a
Continuation
. I also can't
runBlocking
since that uses a new/different context. This is pretty much what I want to do:
Copy code
public open operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
    val ctxElement = coroutineContext[CommandContextElement] ?: error("no command ctx element found")
    val ctx = ctxElement.context ?: error("ctx is unset in ctx element")
    return ctx.get<T>(id)
  }
I was using `ThreadLocal`s before but I feel like that could break with coroutines and the async dispatcher I'm using (MCCoroutine's Bukkit Async Scheduler Dispatcher) which is why I wanted to use something coroutine-specific
y
r
Is there any possible way to do this then? Or would I have to go back to
ThreadLocal
? I really want to keep the delegation-based syntax haha
I could just have a
suspend fun get(): T?
but meh, that's no fun
y
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/ might help. Sadly a function might be your best bet. I really wish we had
suspend
properties man.
r
That might be a bit useful but I don't think there's a way to store arbitrary objects in a thread context (I think this might just be a Log4J thing haha). I really don't wanna store it in b64 or something, that would be ridiculously slow. I guess I'll just either use
ThreadLocal
here or a function
Copy code
context(CommandContext<Source>)
  public open suspend fun value(): T? {
    return get<T>(id)
  }
This works for now 😔
y
Thread context absolutely allows arbitrary elements, and they store it in a thread local automatically
r
Oh yeah that's fair, I could use a thread local combined with thread context elements 🤔
y
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html I think that's what you're looking for. It'll update the thread local to always match the active coroutine's value
So, in a coroutine, accessing this thread context element is the same as accessing the threadlocal
r
So I would call
restoreThreadContext
where I would be setting my context element regularly and then call
updateThreadContext
when I need to access it or how does this work?
y
It seems to work auto-magically, as long as you use
withContext
for your updates (if any)
r
I'm wondering whether a get operator inside of a
CoroutineScope
context receiver would work, will try that once I'm back home 🤔
I did something similar now where I used context receivers like this:
Copy code
context(CommandBuilder) // Where I'm declaring the delegate
  public open operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
    throw UnsupportedOperationException()
  }

  context(CommandExecutionContext) // Where I would like to use it
  public open operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
    return ctx.get<T>(id) // ctx is from the execution context
  }
This may seem a bit hacky, but you can achieve something similar if you have your second context receiver be a
CoroutineScope
y
I'm pretty sure that this sadly doesn't work because it'll capture the
CommandExecutionContext
at the declaration side, and won't actually be getting it at the call-side of the getter
Copy code
import kotlin.reflect.*
fun main() {
    // Fails with 
	// Property delegate must have a 'getValue(Nothing?, KProperty0<String>)' method. None of the following functions is applicable: context(String) fun getValue(thisRef: Any?, property: KProperty<*>): String
    //val foo by Foo
    with("world") {
        val foo by Foo
        with("hi") {
            println(foo) // prints "world"
        }
    }
}
object Foo {
    context(String)
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = this@String
}
r
It compiles fine, but it throws an
UnsupportedOperationException
, that's a bit annoying
y
what version are you on? I'm surprised yours even compiles, but regardless, it sadly doesn't work as we would like it to right now.
r
I'm on 2.1.10
y
Huh strange, well regardless contexts on properties will be temporarily removed (again it really wasn't that useful anyway):
At this point, properties with context parameters may not use delegation. It is unclear at this point what should be the correct semantics for such a declaration.
r
I could go back to using coroutine context elements and then have my CommandBuilder implement CoroutineScope and have some kind of extension or context receiver for the scope with the getter
Actually, just having some common interface may make more sense here
Copy code
public interface ArgumentAware {
  public fun <T : Any> get(id: String): T?

  public operator fun <T : Any> CommandArgument<T>.getValue(thisRef: Any?, property: KProperty<*>): T? {
    return get<T>(id)
  }
}

public class CommandBuilder : ArgumentAware {
  override fun <T : Any> get(id: String): T? {
    throw UnsupportedOperationException("Cannot get an argument in a command builder")
  }
}

public interface CommandExecutionContext : ArgumentAware {
  override fun <T : Any> get(id: String): T? {
    ...
  }
}
This also does not work and just throws the
UnsupportedOperationException
from the CommandBuilder...
Nope, the solution with the coroutine contexts also does not work
y
Yeah, there's sadly no mechanism really to get this to work :(
r
I guess I'll have to use a thread local then, just hoping it won't break with async shenanigans
y
https://github.com/JetBrains/intellij-deps-kotlinx.coroutines/tree/master looks similar to what you want to do. It's actively maintained by JetBrains, and used inside Idea
r
kotlinx.coroutines.internal.intellij.IntellijCoroutines.currentThreadCoroutineContext
does seem very useful, I might use this instead, thanks!
Copy code
public open operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
    val coroutineContext = IntellijCoroutines.currentThreadCoroutineContext() ?: error("not in a coroutine")
    val ctx = coroutineContext[CloudCommandExecutionContext] ?: error("no execution context found")
    return ctx.ctx.get<T>(id)
  }
This works, thanks!
K 1