rabbibotton / clog

CLOG - The Common Lisp Omnificent GUI
Other
1.55k stars 103 forks source link

Synchronization of DOM changes from Lisp code and inline <script>s #378

Open khinsen opened 3 months ago

khinsen commented 3 months ago

When I replace the content of an element via clog:inner-html, any <script> elements with inline code in the supplied HTML are executed, but the detailed rules for this execution are not clear. A quick glance at the html() function in JQuery, which is what clog:inner-html ends up calling, only shows that the rules are likely to be complicated.

If such embedded scripts change the DOM, it is important to wait for the end of their execution before inspecting or manipulation the DOM from CLOG. I tried to do this by adding a <script> element at the end of the HTML code that I injected via clog:inner-html. This script does $(clog['document']).trigger('on-load-script','~A'), where ~A gets replaced by a unique token (a gensym). Then I add a handler for the on-load-script event, which uses a semaphore to signal the end of execution to the Lisp level, using the unique token to distinguish between possibly multiple events.

This mechanism frequently fails, in that the semaphore signal arrives before the scripts embedded in the HTML have terminated. As a consequence, my Lisp code working on the DOM sees some intermediate state.

khinsen commented 3 months ago

Original discussion at https://github.com/rabbibotton/clog/discussions/377

khinsen commented 2 months ago

I did some more testing today and concluded that this has nothing to do with setting inner-html, nor with <script> elements. I get the same problem doing

(clog:js-execute obj "callFunctionThatTakesAWhileAndCreatesElementsInTheDOM()")
(let ((html-id id-of-an-element-that-was-created-by-that-JS-function))
  (clog:js-execute obj (format nil "clog['~A']=$('#~A').get(0)" html-id html-id)))

In my real use case, the second js-execute happens inside clog:attach-as-child. The function that takes a while calls Graphviz to render a graph to SVG.

What happens is that the query by html-id fails, returning undefined. If I insert (sleep 1) between the two calls, it succeeds.

Next, I tried replacing the first js-execute by blocking-js-execute as defined below. That makes no difference, the lookup still fails.

(defun blocking-js-execute (clog-obj js-code &key (wait-timeout 3))
  (let* ((sem (bordeaux-threads:make-semaphore))
         (document (clog:html-document (clog:connection-body clog-obj)))
         (token (symbol-name (gensym)))
         (code-with-trigger
           (format nil "~A;$(clog['document']).trigger('on-load-script','~A')"
                   js-code token)))
    (flet ((on-load (obj received-token)
             (declare (ignore obj))
             (when (equalp token received-token)
               (bordeaux-threads:signal-semaphore sem))))
      (clog:set-on-load-script document #'on-load :one-time t)
      (clog:js-execute clog-obj code-with-trigger)
      (clog:flush-connection-cache clog-obj)
      (bordeaux-threads:wait-on-semaphore sem :timeout wait-timeout))))