google / j2cl

Java to Closure JavaScript transpiler
Apache License 2.0
1.23k stars 144 forks source link

Asynchronous functions in a Java/JavaScript library #144

Closed rbsdc closed 2 years ago

rbsdc commented 2 years ago

Dear J2CL-community,

currently, I am working on a Java library, which I also want to use in JavaScript. The library provides methods to perform operations on certain files. It expects an interface that provides access to the file:

interface FileReader {
  byte[] readRange(int offset, int length);
}

In Java this interface is implemented with RandomAcessFile, in JavaScript with FileReader and Blob::slice. However, there is a problem on the JavaScript part: FileReader reads asynchronously, which is why my solution, as far as I've tried, doesn't work. So far I had two approaches to solve this problem:

I would like to keep it as simple as possible. Is there maybe a solution to make it work with the interface mentioned above? Even if Promises would work in Java, I would rather do without them, if possible, because it unnecessarily increases the complexity of the code. On the other hand, if I mark a method as @JsAsync, is it possible that J2CL adds an await before method calls (this, of course, entails the function containing await is also transpiled as async etc.)? Like this, I would not have to deal with native Promises in Java. Thank you very much in advance.

gkdn commented 2 years ago

Automatically adding await on promises doesn't really emulate the synchronous behavior where there are a lot of edge cases that the developer could observe the behavior difference.

I'm not familiar with FileReaderSync API but if the Webworkers is not an option for you, then it is not possible to provide a synchronous API on top of I/O operations. Note that nearly all client apps in Google architected by assuming that such APIs are async (e.g. via Promises or ListenableFutures). This is generally good for all platforms since you don't want blocking operations on the UI thread.

Hope it helps.

rbsdc commented 2 years ago

Thank you very much for your swift reply!

As for the first point - you are of course right. Adding await to all async method calls when transpiling is far from trivial and might have further unintended implications (in particular, since functions containing await must be async themselves, as mentioned in my initial question). Nonetheless, maybe it would be worth evaluating something like @JsAsync(addAwaitToCalls=true). Perhaps, this would be possible to implement and useful in some cases, although in some cases it would break the code - however, as described in the above mentioned setting, in some cases the code does not work anyway. (and I would assume, my case is by no means contrived but an actual problem).

As for the second point - I am fully aware of blocking operations, which is why I would wrap the library calls in a Promise anyway. However, normally, I think, it is best to let the client (i.e. the JavaScript or Java application that uses the library) decide how to handle these problems. This last point only as a general remark.

To summarize: Do I understand it correctly that it is currently not possible to make my case work both in Java and JavaScript (if we leave web workers aside for a moment)? Or are there other possible solutions?

Thank you again!

niloc132 commented 2 years ago

Note that TeaVM does (roughly) produce async/await in this way, by "recoloring" the functions that are found to call the async function in question. It can't generally work though, there will inevitably be functions that must return synchronously, and where those meet these "re-colored" functions, there must be a compiler error, or wrong code produced. This operation does require whole-world knowledge of the program (so isn't something j2cl could do).

I don't think your idea of addAwaitToCalls=true actually makes sense, since that means that in turn the function which gets the await (i.e. the one calling the method that had the annotation) in turn needs its own @JsAsync to be added to it.


+1 for preferring async styled code generally. Your first bullet point hit on this a bit, but it sounds like you tried to only solve with async/await to emulate blocking instead of staying async through your API - did you consider trying to expose a listener/thenable that would work in the JVM? J2CL's tests include com.google.j2cl.junit.async.AsyncTestRunner, a simple junit4 Runner that lets a JS-compatible Thenable (see com.google.j2cl.junit.integration.async.data.Thenable) be adapted into a guava ListenableFuture, so that the wrapping JVM code can actually block on the results. Or, implement a JS and JVM version of Promise, and actually make reading your data asynchronous in both, with the JVM implementation submitting work to an executor as necessary, etc.

rbsdc commented 2 years ago

Thank you for the reply! I looked at the JS-compatible Thenable and I made it almost work. Maybe this is a bug? If I use the previous interface method byte[] readRange(int offset, int length); (mentioned in the initial post) and FileReaderSync this works fine. However, If use Thenable<byte[]> readRange(int offset, int length); then it leads to a casting error (Something like Error: Class$obf_1003: Class$obf_1001 cannot be cast to [LClass$obf_1002). In both cases I correctly retrieve a Uint8array, which is why I would expect that it works in both cases.

I also tried to wrap the Thenable in a ListenableFuture (more specifically SettableFuture, since listeningDecorator etc. do not seem to work when transpiling with J2CL). This works fine in the JVM, of course, but when I call the get() in the transpiled JavaScript library this also results in an error, because the future is still pending (something I expected too, but I nevertheless tried). Is there maybe an other way? If I would have to work with Thenable, I would prefer to somehow block the result, else the code would be sprawled with Thenables everywhere. Unfortunately, AsyncTestRunner was not very enlightening for this case. Perhaps, I also missed something.

rbsdc commented 2 years ago

I opened a new issue for the possible bug mentioned in the previous comment: https://github.com/google/j2cl/issues/145