Tool of Thought

APL for the Practical Man

"You don't know how bad your design is until you write about it."

Threading the HTMLRenderer

September 22, 2022

The HTMLRenderer supports web sockets, which gives us two-way asynchronous communication between the browser and the APL session. The browser can send a message to APL, and APL can send a message to the browser. However, in neither case is the sender waiting for a reply from the recipient. Much functionality can be achieved this way. However, there are at least two cases when, from APL, we want to send a message to the HTMLRenderer and wait for a response. The first is simply to execute a little general JavaScript. We might want to execute '2+2' or get the innerHTML from an element, or get the HTML of the entire page. This should work anytime, anywhere, and specifically it must work under the event handler of the WebSocketReceive event, which is exactly where it gets tricky.

The second is when, from APL, we want to fire an event in the HTMLRenderer which has been wired with a event handler that calls back into APL. APL handles the message, presumably changing some state, and perhaps sending a message back to the HTMLRenderer, changing some state there. In this case we want to be able to wait in APL for all this to happen, and then verify the state changes. In other words, we want to test our HTMLRenderer GUI code in a easy, linear fashion, in one APL process. This post attempts to explain an attempt to implement this synchronous behavior for these two cases.

The ExecutJavaScriptSync function executes a snippet of JavasScript and waits for a response:

     ⍝ ⍺ ←→ Document
     ⍝ ⍵ ←→ Javascript
     ⍝ ← ←→ Result                                                     
     _←⍺ ExecuteJavaScript j
     ⎕TGET ⎕TID

The left argument is the APLDOM object, the right argument is a string of JavaScript. The thread ID is used as a message identifier.

There is also a cover function that executes a string of javascript inside a specfic element:

     ⍝ ⍺ ←→ Element
     ⍝ ⍵ ←→ Javascript
     ⍝ ← ←→ Result
     i←TagIndex ⍺
     ⍺.Document ExecuteJavaScriptSync c

The ExecuteJavaScript function just sends the data over the socket:

     ⍝ ⍺ ←→ Document
     ⍝ ⍵ ←→ JavaScript
     ⍺.HTMLRenderer.WebSocketSend ⍺.HTMLRenderer.WebSocketID ⍵ 1 1

Note that we do not make use of the HTMLRenderer.ExecuteJavaScript method.

Once the message is sent, we wait for a token (⎕TGET ⎕TID). Meanwhile, in the HTMLRenderer, the following JavaScript is executed on receipt of the message from APL:

  function execCode(id,code) { 
         const b = {Event: "SJSR", id: id, result: eval(code) }; 
         const m = JSON.stringify(b); 
         ws.send (m)

This simply evaluates the code and sends the result and the id back to APL. It also invents and returns an event named "SJSR" for synchronous javascript result.

Back in APL, we are waiting for and processing messages from the HTMLRenderer via the HTMLRenderer's WebSocketReceive event:

     c←⎕JSON 3⊃⍵
     c.Event≡'SJSR':c.result ⎕TPUT
     te ce←(Elements d)[j k]
     h.LastTID←h.LastTID HandleRequest&c

This function must handle messages that are instigated by events in the browser, like clicking a button, as well as messages responding to synchronous calls from APL (the "SJSR" event).

We must allow OnWebSocketReceive to fire continuously without waiting for the event handler in APL (for the click event on a button, say) to complete. Otherwise if the event handler itself sends a synchronous request back to the browser, the system will deadlock, waiting for a response that will never arrive, as OnWebSocketReceive can never fire as it is waiting for the event handler to complete, which is waiting for the response to the synchronous request. Thus we need to thread something, somewhere. But this introduces a new problem. The moment we thread, we allow all browser events to be processed concurrently, when we only want SJSR events processed concurrently with respect to all other events. All events initiated by the browser must be processed sequentially and exclusively, unless of course explicitly threaded somewhere in their own handlers.

It is possible to thread OnWebSocketReceive itself, but this only makes it significantly more difficult to queue the events. Instead we thread at the very end where we call HandleRequest which waits for the previous request to complete before executing:

     _←{6::0 ⋄ ⎕TSYNC ⍵}⍺
     ~EventToken∊⎕TREQ ⎕TNUMS:0
     ⍵ ⎕TPUT EventToken

Because OnWebSockReceive is single-threaded, we are guaranteed that LastTID is updated before the next message is received, and events processed in the proper order.

If the event is "SJSR", that is if the event is the result of a synchronous JavaScript request, we put a token whose value is the result into the pool, signaling to the waiting thread that it now has its requested result. The second case for synchronous behavior is testing. We want to be in APL and initiate an event in the HTMLRenderer that in turn sends a message to APL that does some processing. When this processing is complete, we want to verify some state change. Let's look at a sample test function:

     r←d A.ElementById'result'
     b←d A.ElementById'decrement'
     _←b A.FireEventAndWait'click'

This function first inspects and saves the content of an element, then fires a click event in the HTMLRenderer and waits for the event handler to complete, and finally compares the new value in the element to the expected value. The FireEventAndWait function is:

     ⍝ ⍺ ←→ Element
     ⍝ ⍵ ←→ Event
     _←⍺ ExecuteOnElement ⍵,'()'
     ⎕TGET EventToken

The EventToken is just a constant. Test functions MUST run in their own thread, separate from the thread the HTMLRenderer is using to process WebSocketReceive events, otherwise no events are processed as we sit waiting for EventToken. What now happens as we are waiting to get EventToken? If we review the last couple of lines of HandleRequest above we see that after an event is processed if there is any thread waiting on an EventToken, then it is supplied:

     ~EventToken∊⎕TREQ ⎕TNUMS:0
     ⍵ ⎕TPUT EventToken 

The test framework can thread itself, so all of the tests run in a new thread:

     ⍝ ⍺ ←→ [Namespace of tests]
     ⍝ ⍵ ←→ HTMLRenderer
     ⎕TID=0:⍺ ∇&⍵
     s h←⍺ ⍵
     c←'Passed' 'Failed' 'Broken' 'N/A' 'Disabled'
         b←{0::2 ⋄ (s⍎⍵)h}⍵
         b⊣⎕←⍵,': ',⍕b⊃c
     }¨'T's.⎕NL ¯3

The only reason to thread the event handler in OnWebSocketReceive is to allow for synchronous JavaScript from within the event handler. If synchronous JavaScript is not needed, the & and the ⎕TSYNC may just be removed, and all event handlers run in thread 0, for what that is worth. Tests need to be threaded regardless, and as a byproduct this allows synchronous JavaScript to run in tests even when not threading the event handler.