Skip to content

Commit

Permalink
Replace repeated polling in Outbox.loop() with an asyncio event (see #…
Browse files Browse the repository at this point in the history
…2482) (#2867)

* Add asyncio events to outbox

* Moved event clearing as a minor optimization

* clean up timeout argument

* Change timeout duration

* Handle connection timeout

* Add log import

* remove extraneous whitespace

* code review and improvement

* Add asyncio.TimeoutError for Python < 3.11

* Fix test hangs

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
  • Loading branch information
afullerx and falkoschindler committed Apr 16, 2024
1 parent b7f0ec5 commit a2544c1
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 6 deletions.
24 changes: 20 additions & 4 deletions nicegui/outbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,51 @@ def __init__(self, client: Client) -> None:
self.updates: Dict[ElementId, Optional[Element]] = {}
self.messages: Deque[Message] = deque()
self._should_stop = False
self._enqueue_event: Optional[asyncio.Event] = None
if core.app.is_started:
background_tasks.create(self.loop(), name=f'outbox loop {client.id}')
else:
core.app.on_startup(self.loop)

def _set_enqueue_event(self) -> None:
"""Set the enqueue event while accounting for lazy initialization."""
if self._enqueue_event:
self._enqueue_event.set()

def enqueue_update(self, element: Element) -> None:
"""Enqueue an update for the given element."""
self.updates[element.id] = element
self._set_enqueue_event()

def enqueue_delete(self, element: Element) -> None:
"""Enqueue a deletion for the given element."""
self.updates[element.id] = None
self._set_enqueue_event()

def enqueue_message(self, message_type: MessageType, data: Any, target_id: ClientId) -> None:
"""Enqueue a message for the given client."""
self.messages.append((target_id, message_type, data))
self._set_enqueue_event()

async def loop(self) -> None:
"""Send updates and messages to all clients in an endless loop."""
self._enqueue_event = asyncio.Event()
self._enqueue_event.set()

while not self._should_stop:
try:
await asyncio.sleep(0.01)

if not self.updates and not self.messages:
continue
if not self._enqueue_event.is_set():
try:
await asyncio.wait_for(self._enqueue_event.wait(), timeout=1.0)
except (TimeoutError, asyncio.TimeoutError):
continue

if not self.client.has_socket_connection:
await asyncio.sleep(0.1)
continue

self._enqueue_event.clear()

coros = []
data = {
element_id: None if element is None else element._to_dict() # pylint: disable=protected-access
Expand Down
5 changes: 3 additions & 2 deletions tests/test_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ def test_autocompletion(screen: Screen):
assert element.get_attribute('value') == 'fx'
assert input_.value == 'fx'

input_.set_autocomplete(['one', 'two'])
input_.set_autocomplete(['once', 'twice'])
screen.wait(0.2)
element.send_keys(Keys.BACKSPACE)
element.send_keys(Keys.BACKSPACE)
element.send_keys('o')
screen.should_contain('ne')
screen.should_contain('nce')


def test_clearable_input(screen: Screen):
Expand Down
1 change: 1 addition & 0 deletions tests/test_javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def page():
screen.open('/')
screen.should_contain('before js')
screen.should_contain('after js')
screen.wait(0.5)
screen.should_contain('New Title')


Expand Down

0 comments on commit a2544c1

Please sign in to comment.