33import abc
44from asyncio import (
55 FIRST_COMPLETED ,
6+ CancelledError ,
67 Event ,
78 Queue ,
89 Task ,
910 create_task ,
10- gather ,
1111 get_running_loop ,
1212 wait ,
1313)
2727from uuid import uuid4
2828from weakref import ref as weakref
2929
30+ from anyio import Semaphore
31+
3032from reactpy .config import (
3133 REACTPY_CHECK_VDOM_SPEC ,
3234 REACTPY_DEBUG_MODE ,
@@ -55,6 +57,7 @@ class Layout:
5557 "_event_handlers" ,
5658 "_rendering_queue" ,
5759 "_render_tasks" ,
60+ "_render_tasks_ready" ,
5861 "_root_life_cycle_state_id" ,
5962 "_model_states_by_life_cycle_state_id" ,
6063 )
@@ -73,21 +76,28 @@ async def __aenter__(self) -> Layout:
7376 # create attributes here to avoid access before entering context manager
7477 self ._event_handlers : EventHandlerDict = {}
7578 self ._render_tasks : set [Task [LayoutUpdateMessage ]] = set ()
79+ self ._render_tasks_ready : Semaphore = Semaphore (0 )
7680
7781 self ._rendering_queue : _ThreadSafeQueue [_LifeCycleStateId ] = _ThreadSafeQueue ()
78- root_model_state = _new_root_model_state (self .root , self ._rendering_queue . put )
82+ root_model_state = _new_root_model_state (self .root , self ._schedule_render_task )
7983
8084 self ._root_life_cycle_state_id = root_id = root_model_state .life_cycle_state .id
81- self ._rendering_queue .put (root_id )
82-
8385 self ._model_states_by_life_cycle_state_id = {root_id : root_model_state }
86+ self ._schedule_render_task (root_id )
8487
8588 return self
8689
8790 async def __aexit__ (self , * exc : Any ) -> None :
8891 root_csid = self ._root_life_cycle_state_id
8992 root_model_state = self ._model_states_by_life_cycle_state_id [root_csid ]
90- await gather (* self ._render_tasks , return_exceptions = True )
93+
94+ for t in self ._render_tasks :
95+ t .cancel ()
96+ try :
97+ await t
98+ except CancelledError :
99+ pass
100+
91101 await self ._unmount_model_states ([root_model_state ])
92102
93103 # delete attributes here to avoid access after exiting context manager
@@ -137,40 +147,11 @@ async def _serial_render(self) -> LayoutUpdateMessage: # nocov
137147
138148 async def _concurrent_render (self ) -> LayoutUpdateMessage :
139149 """Await the next available render. This will block until a component is updated"""
140- while True :
141- render_completed = (
142- create_task (wait (self ._render_tasks , return_when = FIRST_COMPLETED ))
143- if self ._render_tasks
144- else get_running_loop ().create_future ()
145- )
146- queue_ready = create_task (self ._rendering_queue .ready ())
147- try :
148- await wait ((queue_ready , render_completed ), return_when = FIRST_COMPLETED )
149- finally :
150- # Ensure we delete this task to avoid warnings that
151- # task was deleted without being awaited.
152- queue_ready .cancel ()
153-
154- if render_completed .done ():
155- done , _ = await render_completed
156- update_task : Task [LayoutUpdateMessage ] = done .pop ()
157- self ._render_tasks .remove (update_task )
158- return update_task .result ()
159- else :
160- model_state_id = await self ._rendering_queue .get ()
161- try :
162- model_state = self ._model_states_by_life_cycle_state_id [
163- model_state_id
164- ]
165- except KeyError :
166- logger .debug (
167- "Did not render component with model state ID "
168- f"{ model_state_id !r} - component already unmounted"
169- )
170- else :
171- self ._render_tasks .add (
172- create_task (self ._create_layout_update (model_state ))
173- )
150+ await self ._render_tasks_ready .acquire ()
151+ done , _ = await wait (self ._render_tasks , return_when = FIRST_COMPLETED )
152+ update_task : Task [LayoutUpdateMessage ] = done .pop ()
153+ self ._render_tasks .remove (update_task )
154+ return update_task .result ()
174155
175156 async def _create_layout_update (
176157 self , old_state : _ModelState
@@ -403,7 +384,7 @@ async def _render_model_children(
403384 index ,
404385 key ,
405386 child ,
406- self ._rendering_queue . put ,
387+ self ._schedule_render_task ,
407388 )
408389 elif old_child_state .is_component_state and (
409390 old_child_state .life_cycle_state .component .type != child .type
@@ -415,15 +396,15 @@ async def _render_model_children(
415396 index ,
416397 key ,
417398 child ,
418- self ._rendering_queue . put ,
399+ self ._schedule_render_task ,
419400 )
420401 else :
421402 new_child_state = _update_component_model_state (
422403 old_child_state ,
423404 new_state ,
424405 index ,
425406 child ,
426- self ._rendering_queue . put ,
407+ self ._schedule_render_task ,
427408 )
428409 await self ._render_component (
429410 exit_stack , old_child_state , new_child_state , child
@@ -458,7 +439,7 @@ async def _render_model_children_without_old_state(
458439 new_state .children_by_key [key ] = child_state
459440 elif child_type is _COMPONENT_TYPE :
460441 child_state = _make_component_model_state (
461- new_state , index , key , child , self ._rendering_queue . put
442+ new_state , index , key , child , self ._schedule_render_task
462443 )
463444 await self ._render_component (exit_stack , None , child_state , child )
464445 else :
@@ -479,6 +460,21 @@ async def _unmount_model_states(self, old_states: list[_ModelState]) -> None:
479460
480461 to_unmount .extend (model_state .children_by_key .values ())
481462
463+ def _schedule_render_task (self , lcs_id : _LifeCycleStateId ) -> None :
464+ if not REACTPY_FEATURE_CONCURRENT_RENDERING .current :
465+ self ._rendering_queue .put (lcs_id )
466+ return None
467+ try :
468+ model_state = self ._model_states_by_life_cycle_state_id [lcs_id ]
469+ except KeyError :
470+ logger .debug (
471+ "Did not render component with model state ID "
472+ f"{ lcs_id !r} - component already unmounted"
473+ )
474+ else :
475+ self ._render_tasks .add (create_task (self ._create_layout_update (model_state )))
476+ self ._render_tasks_ready .release ()
477+
482478 def __repr__ (self ) -> str :
483479 return f"{ type (self ).__name__ } ({ self .root } )"
484480
0 commit comments