How FileMaker.PerformScript triggers scripts
At dotfmp we had a small extra session with Russell Watson, Peter Wagemans, Andries Heylen and Shin talking about building a new JavaScript thing to make using FileMaker.PerformScript easier. You know a few developers came up with code around FileMaker.PerformScript to delay a call until the function is available, to repeat calls if a JavaScript exception is rasied and try again or to find standard ways to work with promises to have the FileMaker script call back to JavaScript in order to acknowledge that it was called successfully.
But in-between, we noticed something else and today I verified this in the office before posting a blog post. FileMaker.PerformScript triggers the script you call immediately. It does not wait for current script to finish and just pushes any current script down on the call stack and puts the new script on top, so it runs directly. That is different to plugins starting scripts as they get added to the queue and start whenever the current scripts are finished.
Order of execution
If you have three scripts named Script1, Script2, Script3 and you trigger them in order, what would you expect to happen if every script shows a dialog box?
function performFileMakerScript() {
FileMaker.PerformScript ( "Script1", "" );
FileMaker.PerformScript ( "Script2", "" );
FileMaker.PerformScript ( "Script3", "" );
}
The dialog boxes show Script3 to run first, then Script2 and finally Script1. This is suspicious as we would expect the order to be honored.
Interrupting longer script
Let's say we have script 5 like this:
Set Variable [ $$log ; Value: $$log & Get(ScriptName) & " " & Get(CurrentTimeUTCMilliseconds) & ¶ ]
So we add script name and time to a global variable.
And we have Script 4 like this:
Set Variable [ $$log ; Value: $$log & Get(ScriptName) & " " & Get(CurrentTimeUTCMilliseconds) & ¶ ]
#
Set Variable [ $Time ; Value: Get(CurrentTimestamp) ]
Set Variable [ $EndTime ; Value: $Time + 20 // +20 seconds ]
Loop
Exit Loop If [ Get(CurrentTimestamp) > $endTime ]
End Loop
#
Set Variable [ $$log ; Value: $$log & Get(ScriptName) & " " & Get(CurrentTimeUTCMilliseconds) & ¶ ]
As you see this script logs the start and end time to our global $$log variable. And it has a loop to waste time for 20 seconds. When you trigger Script4 and then Script5 with a plugin, they run in that order. But not so with FileMaker.PerformScript. Let's say you have some longer script running and we run it from JavaScript. Then a few seconds later a second script is triggers in JavaScript as some asynchronous process finishes (e.g. HTTP request) and this calls FileMaker.PerformScript to report back this. Here some JavaScript to do this:
function performFileMakerScript() {
FileMaker.PerformScript ( "Script4", "" );
setTimeout(function(){
FileMaker.PerformScript ( "Script5", "" );
}, 10000);
}
Now if you do this and see the scripts triggered, the $$log variable may look like this:
Script4 63727115961892
Script5 63727115973893
Script4 63727115982001
If you add further logging, you really see the loop runs a few thousand times, before Script5 runs and then the loop continues a few thousand times.
Real World example
To give you an idea why this may be a problem. Let's say you go on the layout to show a calendar with your coming appointments. When you enter the layout a script runs to let the web view start to load JavaScript based calendar. Loading calendar may take a second or two. The user sees the layout and clicks directly an export button to run a script to export something. While the export script jumps to layouts, fetches records to export something, the web viewer triggers a script. For example it uses FileMaker.PerformScript to trigger the script to inform FileMaker the calendar is ready and requests the list of events. The export script is interrupted right away and the new script runs. It may change the layout, get some records and passes them as JSON to JavaScript via Perform JavaScript script step. While it runs, the context changes as layout may change, some global variables may change, new records may be created or existing edited and the layout may not be restored or at least LastError state changes. The export script continues and may be in wrong layout, e.g. the one to send the exported data to someone via email.
Windows
A test on Windows shows the same behavior for script execution order with Script3 coming first.
Same for the second test, which proofs to me that JavaScript runs on a different thread and continues while FileMaker scripts run, just like with WebKit.
Technical look
So when you post a message in JavaScript, this runs on a different process and is sent via IPC connection to the FileMaker app and calls didPostMessage for the WebKit engine, which triggers didReceiveScriptMessage in WKScriptMessageHandler protocol implementation within FileMaker and then calls CallScript function in the Draco engine. We suspect some flag gets passed to run directly like in a Perform Script Step and not some other flag used for the plugin script triggers, which put the new script on the queue to run as soon as possible.
You can inspect this via Activity Monitor application with Sample command to get a stack trace for running this and see what's called.
Our WebView.AddScriptMessageHandler function in MBS FileMaker Plugin does trigger scripts via plugin API, so the script runs after current script finished.
Conclusion
As I don't think any FileMaker developer out there expects a script to be interrupted to run a script triggered as FileMaker.PerformScript, we'll report this as a product issue to Claris and maybe they can comment whether it's intended or a bug they may fix in the next release. We tried build 19.0.1.116 for this.