I’ve been investigating alternatives to `runBlocki...
# javascript
a
I’ve been investigating alternatives to `runBlocking {}`in Kotlin/JS, and I’m wondering something. Correct me if I’m wrong, but the restriction is basically because in JS bridging between blocking/non-blocking requires using
await
, which is only possible inside
async
functions. This is incongruous with JVM and Native, where it’s always possible to block the thread, and no
async function
keyword is required. So, I was thinking. would it be possible for a Kotlin compiler plugin to edit the Kotlin/JS compiler so it generates a
async function
instead of a regular function wherever
runBlocking {}
is used in a Kotlin/JS function?
so this Kotlin
Copy code
fun main() {
  println(getBlahs());
}

fun getBlahs(): String {
  return "${blah()} and ${blah(3)}"
}

fun blah(): String = "blah"
fun blah(times: Int): String = runBlocking { "blah".repeat(times) }
would get compiled to JavaScript like this:
Copy code
async function main() {
    println(await getBlahs());
}
async function getBlahs() {
    return blah() + ' and ' + (await blah_0(3));
}
function blah() {
    return 'blah';
}
async function blah_0(times) { // 'async' is added to function definition
    return Promise(repeat('blah', times));
}
e
I think if it was possible to implement
runBlocking
by using async/await it would have been done already. I recall that this discussion for KotlinJS has been going on for quite a while, like years probably
Also, in this case it seems you're using a top level async function, I think this approach has several limitations
a
at the moment I’m just curious whether a compiler plugin is able to change the compiled JavaScript code, so Kotlin will compile all Kotlin functions into async functions
c
The concept of "blocking" doesn't really exist in JS, so there can be no
runBlocking
async/await
is non-blocking, like coroutines
The best way to convert between both worlds is to convert through
Promise
a
The concept of “blocking” doesn’t really exist in JS
isn’t
await
effectively the same as
runBlocking {}
? What’s the difference between this JavaScript function
Copy code
async function blah() {
  const data = await foo()
  return data
}
and this Kotlin function?
Copy code
fun blah(): String {
  val data = runBlocking { foo() }
  return data
}
c
No,
await
is the same as calling a
suspend
function normally, or as calling KotlinX.coroutines'
await()
function
In both cases, it means "I'm waiting for this function to end, you can do something else in the meantime".
runBlocking
means "I'm waiting for this function to end, but you are NOT allowed to do anything else. You're waiting with me." On JS, that would make the entire tab unresponsive: no events could be sent anymore, so all buttons etc would do nothing.
a
that sounds like the same thing to me :)
c
It really isn't. One of them breaks your entire website, the other lets it run normally.
Same reason why you are not allowed to use
runBlocking
on the main thread on Android, except on JS there is nothing but the main thread, so it doesn't even compile
a
sorry, I’m not getting what you mean
if Kotlin/JS compiled all functions to async and converted
runBlocking { foo() }
to
await foo()
then (assuming a little more magic 🪄 to create promises?) then it looks like it would work to me
c
Let's imagine you have a simple website with a button that does
window.alert("clicked")
When the website loads, you do
Copy code
GlobalScope.launch {
    delay(10_000)
    window.alert("stopped")
}
If you click on the button, you get the alert immediately. 10 seconds later, the "stopped" alert appears. Now, let's imagine
runBlocking
existed. When the page loads, you do:
Copy code
runBlocking {
    delay(10_000)
    window.alert("stopped")
}
You click on the button. Nothing happens. After 10 seconds, the "stopped" alter appears. When you close it, the "clicked" alert appears. When you use
runBlocking
, everything else in the page must wait for it to end before anything else can happen.
a
When you use
runBlocking
, everything else in the page must wait for it to end before anything else can happen.
yes, that’s fine by me, that’s what I want
c
It's really not what you want. It means each time you do a network call, all buttons in the app stop working until the network call ends.
You can never have two HTTP requests at the same time
a
that’s fine by me
I want that
c
Just have a global
Mutex
then, but seriously your users don't want that
a
I’m my own user :)
c
What are you making?
a
a test utility to load test resources for multiplatform testing over a network. I want to implement the Okio Filesystem, and the functions aren’t marked as
suspend
c
I don't think that's possible. The browser does not have synchronous APIs to do that. The native APIs are callback/promise based, so you can't use them in regular functions
That's not a Kotlin thing btw, you can't do it in JS either
(and it's on purpose by the browser APIs : if you could do it, everyone would make websites that constantly block everything—Android forbids blocking the main thread for the same reason)
a
The native APIs are callback/promise based, so you can’t use them in regular functions
yeah exactly, so I wanted to find out if there’s a way to tell Kotlin/JS to generate async functions
c
You said you want to implement an interface that is just regular functions? You can't
await
inside of them
Even in JS
a
that’s true at the Kotlin level, but in JavaScript I don’t think the interface exists
c
The signature must watch, and it can never be
async
The only exception is if your original interface always returns values by callback/promise/deferred/future/whatever, in which case you can do stuff. But if it returns plain values, it's just not possible (again, on purpose by the W3C)
a
as I understand JavaScript doesn’t have interfaces, though I’m no expert! So as long as the Kotlin code implements the interface, and the JavaScript code works, then it’s okay if the JavaScript code differs and returns a Promise
c
True, but the JS code will have to be
async
/return a promise. Regular functions in your Kotlin interface must be regular JS functions, so you can't call
async
functions inside of them.
a
Regular functions in your Kotlin interface must be regular JS functions
but what if a Kotlin compiler plugin added
async
to all generated JavaScript functions?
c
Then you wouldn't be able to call any Kotlin code in any non-
async
JS function, so you would never be able to do any kind of interop with JS
a
mmm yes, good point. So what if
async
were added selectively, only where necessary?
c
How do you know where it is necessary?
a
even if it was manually, like a
@JsAsync
annotation
c
You still wouldn't be able to add it to an interface implementation, it would need to be on the interface itself
a
there’s probably a way to be clever and do some analysis to determine all call routes, but just running it and see if it works would be fine initially
c
Example:
Copy code
interface Foo {
    fun foo(): String
]

class FooImpl1 {
    override fun foo(): String = "1"
}

class FooImpl2 {
    @JsAsync
    override fun foo(): String = "2"
}

fun bar(impl: Foo): String {
    return impl.foo()
}
How do you compile
bar
? If the argument is
FooImpl1
, it must be a regular JS function (not
async
), but if the argument is
FooImpl2
, then it must be
async
.
(same reason you can't add
suspend
to overriden functions, so we just circled to the same thing)
a
yeah, that’s a good edge case
in my application it wouldn’t happen because I wouldn’t use Foo, I’d be using my custom
expect class FooImpl
, which in jsMain would have the annotations
c
Same problem. What about common code that calls your implementation? It needs to magically know your implementation is
@JsAsync
and add it to somehow. Which means whenever you add/remove
JsAsync
to any function anywhere, your entire codebase changes signature
a
the Kotlin/JS compiler needs to know, but the whole application doesn’t need to know, does it?
anyway, even if there is no annotation there’ll be some way of getting this information to the Kotlin/JS compiler plugin. Even if I hard code it so that it will add
async
to any usage of
Foo.foo()
I’d like to be able to experiment and create a compiler plugin, hard code the functions that need to be made
async
, but I’m not sure if a compiler plugin would be able to add
async
to a function signature…
c
But that's impossible. The functions that need to be
async
are the functions you want, and all the functions that appear above on the call chain. If at any point you call
Copy code
list.forEach {
    yourAsyncFunction()
}
then
List.forEach
must be
async
, and therefore all functions that call it must be too. And now, everything has to be
async
.
a
yeah, that’s right, so the compiler would need to figure out which functions need to be made async. Again, that can either be done in a clever way, or just hard code it
c
I mean, it's not "figure out", it's the entire codebase. The entire standard library needs to be recompiled just for your project, as well as any other library you use.
a
how so? I use the stdlib, the stdlib doesn’t use my library
e.g.
forEach {}
doesn’t need to be made async, because I’d be using it inside one of my own library’s functions
c
All lambdas passed must be
async
too. All functions you call which accept lambdas must be
async
if you call
async
stuff inside of them, so
forEach
,
filter
,
map
etc need to be
async
as soon as a single of their usage calls
async
in them
And of course, if they become
async
, then all their usages must be
async
too, etc
I guess you can find a way not to need that for
inline
functions (same way they don't need to
suspend
), but that's only a small portion of functions
a
sorry, I don’t follow… it wouldn’t be changing the signatures of those functions to change them from
fun operation(block: () -> Unit)
to
fun operation(block: () -> Promise<Unit>)
like, if I had a library function that used a stdlib function with a lambda:
Copy code
fun blah(data: List<String>) {
  data.map { d ->
    fs.foo(d) // async
  }
}
then this would be translated to
Copy code
async function blah(data) {
  data.map { d ->
    await fs.foo(d)
  }
}
(or whatever the correct JS lambda signature equivalent is!)
c
That's only possible if
map
is
inline
and all executions happen during the execution of the function. In all other cases, this code is wrong.
Example:
Copy code
val tasks = ArrayList<() -> Unit>()

// imagine this is in the standard library
fun registerForLater(block: () -> Unit) {
    tasks.add(block)
}

// this is also in the stdlib
fun executeAll() {
    tasks.forEach { it() }
}

// this is your function
@JsAsync
fun blah(data: List<String>) {
    data.forEach {
        registerForLater { fs.foo(it) } ← await ?
    }

    executeAll() // ??
}
Try it with
suspend
, even if
blah
is
suspend
, you can't call
suspend
functions in the lambda of
registerForLater
, for exactly the same reason
a
mmm okay, I think I get it
so there are a few options: either the ‘add async’ compiler plugin throws an error if it can’t add async to a lambda, it’s just forbidden. Or it updates
tasks
and
registerForLater
to add
async
c
The problems are: • first solution: you won't be able to call any code, because you'll hit functions like that everywhere in Kotlin • second solution: it's contagious, so all usages of these functions must be converted as well, as well as their usages, etc
a
yeah, it’s not perfect
option 3: the ‘add async’ plugin adds async to everything
c
But then, you can't call any Kotlin code from any non-async JS code
And since the top-level code in JS can't be async…
a
this is a test util, so it can return
Promise<Unit>
580 Views