Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Exporting Async methods as JS modules

794 views
Skip to first unread message

Steve Hannah

unread,
Mar 24, 2024, 11:55:13 AM3/24/24
to TeaVM
I'm playing around with the new JSModule support to see what is possible.  Very cool so far.

I'm looking for advice on the best way to export async methods.  For example, here is what I'm doing now:

@JSExport
public static JSPromise<String> getString() {
return JSPromise.create((resolve, reject) -> {
new Thread(() -> {
AppComponent appComponent = DaggerAppComponent.create();
RestClient restClient = appComponent.getRestClient();
resolve.accept("Response is " + restClient.getString("https://example.com/string.json").responseBody());
}).start();
});
}
Then I use this from Javascript, like:

import {getString} from './teavm/classes.js';
console.log(await getString());
This works fine, but I'm curious if there is a way to reduce the boilerplate.

The closer I can get to just:
@JSExport
public static String getString() { AppComponent appComponent = DaggerAppComponent.create(); RestClient restClient = appComponent.getRestClient();
    return "Response is " + restClient.getString("...").responseBody();
}
the better.


Any advice appreciated.

Steve

Alexey Andreev

unread,
Mar 31, 2024, 3:54:17 AM3/31/24
to TeaVM
I really don't recommend to use async functions, since they unpredictably affect performance and code size. The only use-case for this feature is compatibility: you already have some legacy code that relies on threads and blocking. So I don't want to encourage regular user to use asyncs by introducing features like the one you want.

Anyway, I think it's quite trivial task to write a helper function that wraps lambda into this JSProise/Thread boilerplate.
воскресенье, 24 марта 2024 г. в 16:55:13 UTC+1, st...@weblite.ca:

Steve Hannah

unread,
Mar 31, 2024, 8:26:29 AM3/31/24
to Alexey Andreev, TeaVM
On Sun, Mar 31, 2024 at 12:54 AM Alexey Andreev <konsol...@gmail.com> wrote:
I really don't recommend to use async functions, since they unpredictably affect performance and code size. The only use-case for this feature is compatibility: you already have some legacy code that relies on threads and blocking. So I don't want to encourage regular user to use asyncs by introducing features like the one you want.

Actually, I'm currently working on a greenfield personal project that I'm using as a vehicle to experiment with TeaVM. So,  I'm trying to develop some best practices for apps and components written in TeaVM.

I had always felt that the async feature of TeaVM was a "killer" feature.  Is there another answer to `async/await`?

E.g. In Javascript if I need to perform a network request to fetch some data, I can do:

const records = await findRecords();

This is really just syntactic sugar around promises.  I.e. equivalent to `findRecords().then(records => ...)`
But it is way nicer than using the raw promise syntax.

If I want to "wrap" the findRecords() method in TeaVM, then AFAIK I have two options:
1. Use TeaVM's @Async functionality (or higher-level Java synchronization primitives that depend on it) to provide a "synchronous" API.
2. Use raw promise syntax.  E.g. findRecords().then(() -> {...}).

Is there a third option that I'm missing here?

 

Anyway, I think it's quite trivial task to write a helper function that wraps lambda into this JSProise/Thread boilerplate.
Yes, This is the approach I took.  A wrapper method.

Best regards

Steve
 

Alexey Andreev

unread,
Mar 31, 2024, 12:41:47 PM3/31/24
to Steve Hannah, TeaVM



I really don't recommend to use async functions, since they unpredictably affect performance and code size. The only use-case for this feature is compatibility: you already have some legacy code that relies on threads and blocking. So I don't want to encourage regular user to use asyncs by introducing features like the one you want.

Actually, I'm currently working on a greenfield personal project that I'm using as a vehicle to experiment with TeaVM. So,  I'm trying to develop some best practices for apps and components written in TeaVM.

I had always felt that the async feature of TeaVM was a "killer" feature.  Is there another answer to `async/await`?

It is a killer feature, but in another sense. Consider you have an application that is already based on Java-style sync approach with threads and synchronization primitives (like Codename One). You want to port this app to JavaScript with as few changes as possible. In this case async is really a killer feature. As well as support for Java and Kotlin in one project.

If you are writing a JavaScript module (not application!) from the beginning, you better not rely on this feature. It's not only unpredictable performance issues. It's also unpredictable behaviour issues. Consider you have a module that exports async method foo and sync method bar. In this case you can't have a guarantee that bar is sync. As soon as you have at least *one* async method, *any* method can become async. In earlier versions of TeaVM I tried to address this issue by introducing `@Sync` annotations with compile-time verification. Unfortunately, in practical cases this produces too much false negatives. In later versions I deprecated `@Sync` annotation.


E.g. In Javascript if I need to perform a network request to fetch some data, I can do:

const records = await findRecords();

This is really just syntactic sugar around promises.  I.e. equivalent to `findRecords().then(records => ...)`
But it is way nicer than using the raw promise syntax.

If I want to "wrap" the findRecords() method in TeaVM, then AFAIK I have two options:
1. Use TeaVM's @Async functionality (or higher-level Java synchronization primitives that depend on it) to provide a "synchronous" API.
2. Use raw promise syntax.  E.g. findRecords().then(() -> {...}).

Is there a third option that I'm missing here?

Third option is: use Kotlin. Without direct support in language syntax it's impossible to create a reliable alternative to async/await functionality in JS. Also, Java bytecode has some limitations that make it difficult to properly describe verification errors. For example, Java bytecode only contains line numbers/file names for those line, that are executable, but for abstract methods it's not possible to get source line number.

For CoSpaces we write some low-level performance critical code in Java, and higher-level business logic in Kotlin. We don't rely on TeaVM's async support. Instead, we wrote couple coroutine functions like following:

fun <T> async(block: suspend () -> T): JSPromise<T> = JSPromise { resolve, reject ->
    block.startCoroutine(Continuation(EmptyCoroutineContext) { result ->
        result.onSuccess(resolve).onFailure { reject(it) }
    })
}

suspend fun <T> JSPromise<T>.await(): T = suspendCoroutineUninterceptedOrReturn { cont ->
    then { cont.resumeWith(Result.success(it)) }
            .catchError { cont.resumeWith(Result.failure(it.asThrowable())) }
    COROUTINE_SUSPENDED
}

Another option is to use raw promise syntax. According to my experience, *most* async calls just update UI on promise resolution, and there's no need in something non-trivial.

Steve Hannah

unread,
Mar 31, 2024, 1:26:04 PM3/31/24
to Alexey Andreev, TeaVM
What do you think about putting "await" calls inside a JSBody annotation?  

Will this cause problems for TeaVM?

Reply all
Reply to author
Forward
0 new messages