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:
ExecuteJavaScriptSync←{
⍝ ⍺ ←→ Document
⍝ ⍵ ←→ Javascript
⍝ ← ←→ Result
c←'"',⍵,'"'
j←'execCode(',(⍕⎕TID),',',c,')'
_←⍺ 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:
ExecuteOnElementSync←{
⍝ ⍺ ←→ Element
⍝ ⍵ ←→ Javascript
⍝ ← ←→ Result
i←TagIndex ⍺
t←⍺.Tag
c←'document.getElementsByTagName'
c,←'(''',t,''').item(',(⍕i),').',⍵,';'
⍺.Document ExecuteJavaScriptSync c
}
The ExecuteJavaScript
function just sends the data over the socket:
ExecuteJavaScript←{
⍝ ⍺ ←→ 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:
OnWebSocketReceive←{
h←⊃⍵
c←⎕JSON 3⊃⍵
c.Event≡'SJSR':c.result ⎕TPUT c.id
d←h.Document
q←d.StopPropagation
d.StopPropagation←0
q:0
c.HTMLRenderer←h
j←c.TargetIndex
k←c.CurrentTargetIndex
te ce←(Elements d)[j k]
c.Target←te
c.CurrentTarget←ce
h.LastTID←h.LastTID HandleRequest&c
0
}
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:
HandleRequest←{
_←{6::0 ⋄ ⎕TSYNC ⍵}⍺
_←(⍎⍵.CurrentTarget⍎'On',⍵.Event)⍵
~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:
TestDecrement←{
d←⍵.Document
r←d A.ElementById'result'
v←⍎⊃r.Content
b←d A.ElementById'decrement'
_←b A.FireEventAndWait'click'
(v-1)≠⍎⊃r.Content
}
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:
FireEventAndWait←{
⍝ ⍺ ←→ Element
⍝ ⍵ ←→ Event
⍺=0:0
_←⍺ 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:
RunTests←{
⍝ ⍺ ←→ [Namespace of tests]
⍝ ⍵ ←→ HTMLRenderer
⍺←⊃⎕RSI
⎕TID=0:⍺ ∇&⍵
s h←⍺ ⍵
c←'Passed' 'Failed' 'Broken' 'N/A' 'Disabled'
r←{
b←{0::2 ⋄ (s⍎⍵)h}⍵
b⊣⎕←⍵,': ',⍕b⊃c
}¨'T's.⎕NL ¯3
n←¯1+{≢⍵}⌸r,⍨⍳≢c
⍉↑(c,⊂'Total')(n,+/n)
}
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.