Hi, What are some strategies I can employ to chan...
# multiplatform
a
Hi, What are some strategies I can employ to change my runBlocking code to async/await so that it can be compiled to JS? Thanks!
I know this is kinda a noob question, but I may be missing sight of something, that is obvious to somebody else.
c
It will depend on how you are using runBlocking. If you are coming from a JVM background, you could think of moving all the code inside the runBlocking into it's own function. With that then you could use coroutines and dispatchers to move things across different contexts. But generally I find that most use-cases that use runBlocking can be expressed with coroutines.
πŸ‘ 1
a
@mkrussel this is what I implemented, but it seems that the JS version of runBlocking doesn't have the expected behaviour. Also, in retrospect, it looks like the actual fun simply returns a promise so it doesn't "block" the thread.
b
You can't block a thread on js as it's single-threaded already
Easiest solution to get rid of runBlocking in mpp project is to elevate your entite app into suspend scope by changing the entrypoint to suspend fun main() { ... }
βž• 1
πŸ‘ 1
a
@Big Chungus yeah, I know. πŸ™‚ So, conceptually, I cannot do what I'm doing in Java/Kotlin, in JS.
b
Correct
a
@Big Chungus I read somewhere else about the suspend fun main(), but this is a library that I'm producing. So not sure how I'm going to pull that off.
b
In that case make all your library entrypoints suspend in commonMain and provide runBlocking-based alternatives for jvm sourceSet only
a
Let me try @CRamsan's suggestion, even though it will result in A LOT of refactoring. πŸ™‚
πŸ‘ 2
b
Those would just proxy into common suspend functions
c
Suspending from main is just a main function with a runBlocking inside. What you should look at is to remove your dependency on runBlocking if you want to have your code compile for JS. I generally discourage the use of runBlocking even in JVM because coroutines are more configurable and do not block the thread to achieve the same result πŸ™‚.
a
Thanks guys!
b
@CRamsan that's not correct as there's no runBlocking in js yet suspend main still works
πŸ‘Œ 1
c
Yes your are correct. I misunderstood the statement πŸ˜›
e
also
suspend fun main()
on JVM isn't exactly the same as
fun main() = runBlocking { ... }
, due to challenges with
kotlinx.coroutines.runBlocking
being in a separate library. instead it translates to
kotlin.coroutines.jvm.internal.runSuspend
, which is simpler and doesn't set up the event-loop dispatcher that
runBlocking
does
a
@CRamsan since I'm writing a library and not an application, how would I use a coroutine in lieu of runBlocking if I need to wait for a result? This function wouldn't be a suspend function since it will be called by the application using the library.
b
Why can't it be suspend function? Leave it up to the consumer to enter suspend scope. Your main concern should be making it
main-safe
(i.e. switching to appropriate dispatcher for IO and other long-running tasks)
a
How would a Java app use a suspend function?
(That may be a noob question, so please forgive me)
b
Java app can still use runBlocking
a
I see.
b
Alternativelly you can provide shortcut non-suspend functions on jvmMain
a
What do you mean by that?
A function with a runBlocking that calls the suspend?
b
Copy code
// commonMain
suspend fun doStuff(arg1: Any) {...}

//jvmMain
fun doStuffBlocking(arg1: Any) = runBlocking{ doStuff(arg1) }
a
That's what I have now
My problem is ALSO targeting JS
b
So what's the problem? As long as your MPP API surface is all suspend, I see no issues providing some extra utilities for jvmMain only
Note that doStuffBlocking is only in jvmMain sourceSet, therefore will only be available on sourceSets targeting JVM on consumer-end as well
a
Right, but runBlocking won't compile for JS
b
See my message above
a
So
b
no runBlocking in commonMain
Only on jvmMain
a
Got it
b
Therefore js consumers won't see doStuffBlocking
m
You can use extension functions to add platform specific APIs to a common class.
a
Would you suggest expect/actual strategy as well, or just a wrapper in the jvm source set?
b
Likewise, you could provide js helpers too
Copy code
// jsMain
fun doStuffPromise(arg1: Any) = GlobalScope.launch {doStuff(arg1)}.asPromise()
I'd suggest limiting usage of expect/actual as much as possible. Most of the use-cases can be solved via interfaces or generic common api + platform-specific overloads/extensions
m
If you cannot implement it in all platforms, expect/actual doesn't make sense. With extension functions, you don't need to add a wrapper, you just add an extra function to the class
a
Thank you, all! This was very enlightening! This is the kind of help that's needed in the documentation. I'm sure one can figure it out, eventually, but it's like pulling teeth with a pair of tweezers, rather than pliers. :-)
b
yeah, unfortunatelly there's no documented "best practices" for MPP just yet πŸ˜„
Everything is only communicated verbally here on this slack πŸ˜„
a
"Tribal knowledge" is what it's called. We have the same thing at my company and my job, literally, is to break that up!
b
Wasn't aware there's a proper term for that! Good to know!
a
But I got caught up writing this library and haven't been very active in that regard.
So, to recap: suspend all those functions, then provide extensions in the platform specific sourcesets, correct?
<-- (that's my Tribal Knowledge dispeller question πŸ™‚ )
b
Pretty much.
In other words: β€’ Least common denominator in commonMain β€’ Appropriate enchancements in platform sourceSets
βž• 1
πŸ‘† 1
πŸ‘ 1
a
Hi @Big Chungus, I refactored my class based on your suggestion. So one of the functions, which I marked as suspend was protected, but the extension function doesn't have visibility, so I had to make it public. But now I get a complaint that suspend functions are not JsExport-able (which I knew already). If I apply @JsExport to the public functions of the class, would I be able to see the class in JS, since there's no @JsExport on the class itself?
b
Not sure. Give it a go and inspect generated ts declarations
a
@CRamsan, you mentioned the following "...I find that most use-cases that useΒ runBlockingΒ can be expressed with coroutines..." I may need your assistance on some refactoring, but let me explain the situation: 1. Writing a lib that will be consumed by Java and JavaScript developers. 2. Java developers DON'T want to write asynchronously 3. Not an issue with JavaScript (they have no choice anyway) So all my runBlocking code, is, really, just a way to mostly call ktor features and wait for a result synchronously. Because of this, I am forced to suspend some of my lib's functions, naturally. I tried the strategy outlined by @Big Chungus, but that proved to complicated in the long run, so I settled on @mkrussel's suggestion, which, for the most part works. However, if I have the following in my common code:
Copy code
fun something() {
    //do something
    doSomethingElse()
}

fun doSomethingElse() = runBlockingCode{
     //call some suspend features, etc.
}

where

//JS
val testScope = MainScope()
actual fun <T> runBlockingCode(block : suspend () -> T) : dynamic = testScope.apply { }.async(start = CoroutineStart.UNDISPATCHED) { block() }

and

//JVM
actual val coroutineContext : CoroutineContext =
    Executors.newSingleThreadExecutor().asCoroutineDispatcher()
actual fun <T> runBlockingCode(block : suspend () -> T) =
    runBlocking(coroutineContext) { block() }
and then I call
doSomething()
like so...
Copy code
//TypeScript
(async () => {
    await this.myObject.doSomething();
    //get result of doSomething() and do more TypeScripty stuff
}
the code in
doSomething()
runs fine, but when it hits
doSomethingElse()
, it merely gets the reference to the Promise, and not execute. I tried forcing the functions to execute immediately using
CoroutineStart.UNDISPATCHED
, as you see above, but I'm not seeing the code run either. So given the requirements, how would one go about swapping out `runBlocking`and using "normal" coroutines while achieving synchronicity in JVM and asynchronicity in JS? Thanks!
e
Copy code
// common
suspend fun doWork(): T = ...
// js
@JsExport
fun doWorkAsync(): Promise<T> = GlobalScope.promise { doWork() }
// jvm
fun doWorkAsync(): ListenableFuture<T> = GlobalScope.future { doWork() }
fun doWorkBlocking(): T = runBlocking { doWork() }
c
I am not sure I understand your example. It seems like
runBlockingCode
actually does not run blocking code, the oposite, it runs suspending code. If your developers dont want to write suspending code, they can wrap a call to
suspend fun doSomethingElse()
in a runBlocking or you could provide a JVM only wrapper like:
Copy code
fun doSomethingElseBlocking() = runBlocking {
    doSomethingElse()
}
With regards to your tests, in your JS code you are calling async (actual fun <T> runBlockingCode(block : suspend () -> T) : dynamic = testScope.apply { }.*async*(start = CoroutineStart.UNDISPATCHED) { block() }), which indeed returns a reference to the job. The github issue you linked has the right syntax. It should be something like
testScope.promise { this.block() }
and that will run the promise in the testScope.
a
Hi @CRamsan, my runBlockingCode
Copy code
actual fun <T> runBlockingCode(block : suspend () -> T) =
    runBlocking(coroutineContext) { block() }
Is the same as in the github post. Though, I do agree with you that it is running suspended code instead of blocking code. It would be nice if I could remove the suspend from the
(block: suspend () -> T)
so that in the JS version, my function would be
Copy code
actual fun <T> runBlockingCode(block : () -> T) : dynamic = block()
since JavaScript developers are used to async/await/Promise, but, of course, the compiler complains about calling suspend functions outside of a suspend or coroutine.
Personally, I don't mind having to do what @ephemient suggests, but that kind goes against how most developers want to write their code. Especially, in my company.
e
unfortunately exposing a raw Job to JS isn't very useful, so you pretty much do have to wrap things
a
Well, I was thinking that in JS, I would always use async/await or promise, etc. So that would be my wrapper
Or doSomething().then()... sort of thing
e
CoroutineScope.promise(suspend fun) is the wrapper, that returns a Promise that will properly wait until all children have been dispatched
isn't that the answer to "I tried forcing the functions to execute immediately usingΒ CoroutineStart.UNDISPATCHED, as you see above, but I'm not seeing the code run either."?
a
Yes, but it seems the Promise doesn't execute when it's being called from the common code
At least, in the JS side of things
Perhaps I'm trying to put a square peg in a round hole, but it shouldn't be difficult to get this to work. Obviously, I'm missing something fundamental.
I wish I could show you my code, but if I do, I'd have to send James Bond over to do some "dirty work" afterwards. :-)
(Which, BTW, I'm watching tonight)