lona icon indicating copy to clipboard operation
lona copied to clipboard

Frontend-/Backend synchronisation error?

Open SmithChart opened this issue 3 years ago • 3 comments
trafficstars

While working on a non-public lona application I discovered a possible bug in the backend frontend synchronization. I have tried to break it down into a somewhat minimal reproducer. I'll show my reproducer and the effects I have found while creating the reproducer. Turns out: There are cases where this code (with some minimal changes) works as expected.

Here is my reproducer as a Lona Script:

from lona.html import HTML, Div, Button, TextArea, Widget
from lona import LonaApp, LonaView


class Modal(Widget):
    def __init__(self, title='', body=''):
        self.title = Div(title)
        self.body = Div(body)
        self.buttons = []
        self.model_footer = Div()
        self.nodes = [self.body, self.model_footer]
        self.hide()

    def set_body(self, *nodes):
        self.body.nodes = [*nodes]

    def set_buttons(self, *nodes):
        self.buttons = [*nodes]
        self.model_footer.nodes = [*nodes]

    def show(self):
        self.nodes[0].style['display'] = 'block'

    def hide(self):
        self.nodes[0].style['display'] = 'none'

app = LonaApp(__file__)

@app.route('/')
class MyView(LonaView):
    def handle_request(self, request):
        self.comment = TextArea(placeholder='Editor Comment', bubble_up=True)
        self.modal = Modal()
        self.modal.set_body(self.comment)

        # If we create the button here everything works as expected
        #self.modal.set_buttons(
        #    pb := Button('Save', disabled=True),
        #)

        html = HTML(
            Button('Save', _id='show-save-modal'),
            self.modal,
        )

        while True:
            input_event = self.await_click(html=html)

            # creating the button here triggers the error
            self.modal.set_buttons(
                pb := Button('Save', disabled=True),
            )

            self.modal.show()
            while True:
                input_event = self.await_input_event(html=html)
                comment = self.comment.value
                if input_event.node == self.comment:

                    # changes of setting pb.disabled are not propageted
                    # to the frontend correctly
                    if comment.strip():
                        pb.disabled = False
                    else:
                        pb.disabled = True

                        # setting a text here raises the exceptions
                        pb.set_text("Huba")

app.run(port=8080)

Steps to reproduce:

  • Run the server python3 demo.py
  • Got to http://localhost:8080/
  • Click Save. The Widget with the TextArea and a new "Save"-button is displayed as expected.
  • Enter some text. I would assume that the new "Save"-Button gets enabled. But this does not happen.
  • Delete the text in the TextArea. Now I get the following exception:
======== Running on http://localhost:8080 ========
(Press CTRL+C to quit)
MainThread                     INFO     08:31:02.541475 aiohttp.access 127.0.0.1 [25/Aug/2022:06:31:02 +0000] "GET / HTTP/1.1" 200 873 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
MainThread                     INFO     08:31:02.616687 aiohttp.access 127.0.0.1 [25/Aug/2022:06:31:02 +0000] "GET /static/lona/style.css HTTP/1.1" 200 231 "http://localhost:8080/" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
MainThread                     INFO     08:31:02.617115 aiohttp.access 127.0.0.1 [25/Aug/2022:06:31:02 +0000] "GET /static/lona/lona.js HTTP/1.1" 200 250 "http://localhost:8080/" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
LonaWorker_0                   ERROR    08:31:08.892838 lona.view_runtime client error raised:
Lona.LonaDomUpdater/this._insert_node@http://localhost:8080/static/lona/lona.js:417:16
Lona.LonaDomUpdater/this._apply_patch_to_child_nodes@http://localhost:8080/static/lona/lona.js:820:18
Lona.LonaDomUpdater/this._apply_patch@http://localhost:8080/static/lona/lona.js:843:18
Lona.LonaWindow/this._show_html/</<@http://localhost:8080/static/lona/lona.js:1577:44
Lona.LonaWindow/this._show_html/<@http://localhost:8080/static/lona/lona.js:1570:22
Lona.JobQueue/this.add@http://localhost:8080/static/lona/lona.js:70:27

LonaRuntimeWorker_0            ERROR    08:31:08.893554 lona.view_runtime Exception raised while running <__main__.MyView object at 0x7ff17ec78df0>
  Traceback (most recent call last):
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/view_runtime.py", line 318, in start
      raw_response_dict = self.view.handle_request(self.request) or ''
    File "/home/chris/work/Projects/intern/lag-intranet/demo.py", line 57, in handle_request
      input_event = self.await_input_event(html=html)
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/view.py", line 220, in await_input_event
      return self._await_specific_input_event(
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/view.py", line 203, in _await_specific_input_event
      return self._view_runtime.await_input_event(
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/view_runtime.py", line 646, in await_input_event
      return self.server.run_coroutine_sync(_await_input_event())
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/server.py", line 245, in run_coroutine_sync
      return future.result()
    File "/usr/lib/python3.9/concurrent/futures/_base.py", line 440, in result
      return self.__get_result()
    File "/usr/lib/python3.9/concurrent/futures/_base.py", line 389, in __get_result
      raise self._exception
    File "/home/chris/work/Projects/intern/lag-intranet/env/lib/python3.9/site-packages/lona/view_runtime.py", line 644, in _await_input_event
      return await future
  lona.errors.ClientError

Without pb.set_text("Huba") the pb.disabled state is simply not reflected into the frontend.

When I declare the button before the first call to self.await_click() (see comment in reproducer) it works as expected.

Tested on lona==1.10.3.

From a user's perspective I would assume that I am not able to break the frontend backend synchronization. I have had a look into that code but could not find anything obvious.

SmithChart avatar Aug 25 '22 07:08 SmithChart

Hi @SmithChart,

thanks for reporting! This is a bug in the JavaScript rendering engine, and it seems to be completely client side. It is reproducible with this even smaller example:

from lona import LonaApp, LonaView
from lona.html import Div

app = LonaApp(__file__)


@app.route('/')
class MyView(LonaView):
    def handle_request(self, request):
        html = Div(
            Div(),
        )

        self.show(html)

        html.nodes = [
            Div('div'),
        ]

        self.show()

        html.nodes[0].set_text('foo')

        self.show()

        print(html)

app.run(port=8080)

I debugged a little bit and think i understand the underlying issue: Lona renders a new node, with the node-id '3' in this case, and adds it to the dom. This add operation is an RESET in this case. That's important because in case of RESET, the node cache gets cleared. That means the newly created node gets added to the dom without being accessible to the update mechanism. When a patch for the orphaned node comes in, the node id lookup fails and the client crashes.

I think it should not be that hard to fix, and i will start working on it tomorrow.

fscherf avatar Aug 25 '22 19:08 fscherf

Thanks for looking into that!

SmithChart avatar Aug 26 '22 08:08 SmithChart

@SmithChart: I created a PR to resolve this issues

fscherf avatar Aug 26 '22 18:08 fscherf