archi-scripting-plugin
archi-scripting-plugin copied to clipboard
Impossible to write a script with async elements
jArchi Version
1.7
Archi Version
5.4.2
Operating System
Linux Mint Cinnamon 64bit
Description
Async scenarios lead to the following checkmate :
- Case 1: If a promise is pending, but the synchronous part of the code is finished, the entire script is ended, before the promise can finish
- Case 2: If a top level await is written,
org.graalvm.polyglot.PolyglotExceptionis raised
Steps to reproduce
-
Case 1:
console.log("starting synchronous part"); new Promise((resolve) => { console.log("starting async part"); setTimeout(resolve, 1000); }).then(() => console.log("async part ended")); console.log("synchronous part ended");In this case, the JArchi console only prints:
"starting synchronous part""starting async part""synchronous part ended"
=>
"async part ended"is never printed -
Case 2:
console.log("starting synchronous part"); await new Promise((resolve) => { console.log("starting async part"); setTimeout(resolve, 1000); }) console.log("async part ended");There, the console displays the following error:
org.graalvm.polyglot.PolyglotException: SyntaxError: Test.ajs:3:6 Expected ; but found new await new Promise((resolve) => { ^ Test.ajs:6:0 Expected eof but found } }).then(() => console.log("async part ended")); ^ at <js>.:program(<eval>:1) at org.graalvm.polyglot.Context.eval(Context.java:399) at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:478) at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:446) at java.scripting/javax.script.AbstractScriptEngine.eval(Unknown Source)
This might be a limitation in the GraalVM engine (jArchi uses version 22.3.0) or it might be something else. If a JS expert could help here that would be nice.
From what I understand, this may be solveable with a bit of inspiration from this article.
The gist of it would be :
- Either create or modify one of JArchi's Java classes to include a
thenmethod :public class ThenableObject { void then(Value resolve, Value reject) { try { resolve.executeVoid(); } catch (Throwable t) { reject.executeVoid(t); } } } - From the
.ajsscript, invoke said class, and delegate the promise to it:const javaExecutor = Java.type('ThenableObject').createNew(); const interopPromise = new Promise(javaExecutor); // register some promise reactions interopPromise .then( (result) => console.log(`Java resolves: ${result}`), (error) => console.log(`Java rejects: ${error}`) )
I'd like to take a go at my proposal above, but I have no experience in java, and I have no idea how to compile the JArchi project
Hi,
I'll have a look at it.
Hello again!
With help from the documentation and Graal's own unit tests, I have attempted the following :
- I have created a new file
/com.archimatetool.script/src/com/archimatetool/script/commands/AsyncThing.java:package com.archimatetool.script.commands; import org.graalvm.polyglot.Value; import java.util.function.Consumer; import org.graalvm.polyglot.*; public class AsyncThing { @HostAccess.Export public void callFn(Value asyncFunction) { Value jsPromise = asyncFunction.execute(); Consumer<Object> javaThen = (v) -> System.out.println(v.getClass().toString() + "\n" + v.toString()); jsPromise.invokeMember("then", javaThen); Consumer<Object> javaCatch = (v) -> System.err.println(v.getClass().toString() + "\n" + v.toString()); jsPromise.invokeMember("catch", javaCatch); } @HostAccess.Export public void handlePromise(Value jsPromise) { Consumer<Object> javaThen = (v) -> System.out.println(v.getClass().toString() + "\n" + v.toString()); jsPromise.invokeMember("then", javaThen); Consumer<Object> javaCatch = (v) -> System.err.println(v.getClass().toString() + "\n" + v.toString()); jsPromise.invokeMember("catch", javaCatch); } }-
On the .ajs side, those two test scripts:
Test Scripts const AsyncThing = Java.type("com.archimatetool.script.commands.AsyncThing"); const asyncHandler = new AsyncThing();console.log("starting"); asyncHandler.handlePromise( new Promise((r) => setTimeout(r, 1000)) .then(() => console.log("done")); );asyncHandler.callFn(async () => { console.log("starting"); await new Promise((r) => setTimeout(r, 1000)) console.log("done"); });Script Console Output starting ${\color{red}\textsf{class \ com.oracle.truffle.polyglot.PolyglotMap}}$ ${\color{red}\textsf{[object \ Error]}}$ starting ${\color{red}\textsf{class \ com.oracle.truffle.polyglot.PolyglotMap}}$ ${\color{red}\textsf{[object \ Error]}}$
-
As you can see, an error gets thrown uppon launching an async process. The error doesn't really tell me what's going on either ...
I'll try some other code ideas, but I have a feeling that this issue might be solved by switching to a newer version of Graal/GraalJS. However, I'm not a Java dev, so I don't have the required skillset to attempt this. I've been struggling hard to even get the Eclipse project going ...
@jbsarrodie, have you had the time to take a look at this issue ? Do you have some leads ?
One thing that concerns me is whether using asynchronous code will mess up the order of commands. This is important for undo/redo and getting things in the right order.
In my case, I'm tasked with creating a JArchi plugin that generates a PowerPoint presentation from Archi's model. So it's not really a command that interacts with archi, but something that reads Archi's data to do other things on the side.
but I have a feeling that this issue might be solved by switching to a newer version of Graal/GraalJS.
I've pushed a new branch graalvm-latestversion with the latest version of GraalVM. I tested your Case 1 and 2 snippets and got the same result.
Okay, so uh ... The Java code I posted 4 hours ago actually works, It's just that GraalJS doesn't support setInterval.
With that in mind, @Phillipus, if you try the same .ajs snippets, but add this code at the very top, they should both work :
const JavaThread = Java.type("java.lang.Thread");
var setTimeout = (callback, ms, ...args) => {
JavaThread.sleep(ms)
if (args.length) callback(...args)
else callback()
}
Hi,
@jbsarrodie, have you had the time to take a look at this issue ? Do you have some leads ?
I've just had a look and:
- As you noticed, setTimeout doesn't exist in a polyglot context.
- So I updated an old Nashorn polyfill for it to work on graalvm
- And I reproduced your case 1...
- But this setTimeout polyfill implementation is based on java.util.Timer, which creates a new thread...
- In the meantime you shared a polyfill based on JavaThread.sleep, so I tested the case 1 with it and it now works on my side with original (but old) graalvm version !
So it it seems that for case 1 it was only related to setTimeout
For case 2, I can reproduce the error, even with a slightly more recent version of graalvm (22.3.4) that I use for some other purposes.
I've pushed a new branch
graalvm-latestversionwith the latest version of GraalVM. I tested your Case 1 and 2 snippets and got the same result.
So it seems that a recent version of graalvm is not enough to solve case 2.
BTW, @Phillipus we should really upgrade graalvm, several issues that I faced in the past were related to it, so I used a patched version. Because I was using an old version of jArchi (1.5), I didn't noticed 1.7 was still using the same. In addition, we should also add chromeinspector and profiler (both part of graalvm) to make it possible for power users to enable the builtin debugger: I've managed to enable it through a script, the devtools URL is then printed on the terminal/cmd used to start Archi. This is more than enough for power users (and saved me recently to debug a tough issue).
One thing that concerns me is whether using asynchronous code will mess up the order of commands. This is important for undo/redo and getting things in the right order.
It shouldn't have any impact, because all changes are done inside the same thread and recorded in the undo stack in the order they were really done. I did some basic tests that tend to prove it.
Hi @Phillipus @jbsarrodie,
Do you think you'll implement a similar class in principle to what I have proposed Friday into a future release of JArchi ?
I can get Case 1 and Case 2 working by (a) providing the setTimeout function above and (b) wrapping case 2 in an async function:
const JavaThread = Java.type("java.lang.Thread");
var setTimeout = (callback, ms, ...args) => {
JavaThread.sleep(ms)
if (args.length) callback(...args)
else callback()
}
console.log("case 1. starting synchronous part");
new Promise((resolve) => {
console.log("case 1. starting async part");
setTimeout(resolve, 1000);
}).then(() => console.log("case 1. async part ended"));
console.log("case 1. synchronous part ended");
async function start() {
await new Promise((resolve) => {
console.log("case 2. starting async part");
setTimeout(resolve, 1000);
})
console.log("case 2. async part ended");
}
console.log("case 2. starting synchronous part");
start();