3
3
import abc
4
4
from asyncio import (
5
5
FIRST_COMPLETED ,
6
+ CancelledError ,
6
7
Event ,
7
8
Queue ,
8
9
Task ,
9
10
create_task ,
10
- gather ,
11
11
get_running_loop ,
12
12
wait ,
13
13
)
27
27
from uuid import uuid4
28
28
from weakref import ref as weakref
29
29
30
+ from anyio import Semaphore
31
+
30
32
from reactpy .config import (
31
33
REACTPY_CHECK_VDOM_SPEC ,
32
34
REACTPY_DEBUG_MODE ,
@@ -55,6 +57,7 @@ class Layout:
55
57
"_event_handlers" ,
56
58
"_rendering_queue" ,
57
59
"_render_tasks" ,
60
+ "_render_tasks_ready" ,
58
61
"_root_life_cycle_state_id" ,
59
62
"_model_states_by_life_cycle_state_id" ,
60
63
)
@@ -73,21 +76,28 @@ async def __aenter__(self) -> Layout:
73
76
# create attributes here to avoid access before entering context manager
74
77
self ._event_handlers : EventHandlerDict = {}
75
78
self ._render_tasks : set [Task [LayoutUpdateMessage ]] = set ()
79
+ self ._render_tasks_ready : Semaphore = Semaphore (0 )
76
80
77
81
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 )
79
83
80
84
self ._root_life_cycle_state_id = root_id = root_model_state .life_cycle_state .id
81
- self ._rendering_queue .put (root_id )
82
-
83
85
self ._model_states_by_life_cycle_state_id = {root_id : root_model_state }
86
+ self ._schedule_render_task (root_id )
84
87
85
88
return self
86
89
87
90
async def __aexit__ (self , * exc : Any ) -> None :
88
91
root_csid = self ._root_life_cycle_state_id
89
92
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
+
91
101
await self ._unmount_model_states ([root_model_state ])
92
102
93
103
# delete attributes here to avoid access after exiting context manager
@@ -137,40 +147,11 @@ async def _serial_render(self) -> LayoutUpdateMessage: # nocov
137
147
138
148
async def _concurrent_render (self ) -> LayoutUpdateMessage :
139
149
"""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 ()
174
155
175
156
async def _create_layout_update (
176
157
self , old_state : _ModelState
@@ -403,7 +384,7 @@ async def _render_model_children(
403
384
index ,
404
385
key ,
405
386
child ,
406
- self ._rendering_queue . put ,
387
+ self ._schedule_render_task ,
407
388
)
408
389
elif old_child_state .is_component_state and (
409
390
old_child_state .life_cycle_state .component .type != child .type
@@ -415,15 +396,15 @@ async def _render_model_children(
415
396
index ,
416
397
key ,
417
398
child ,
418
- self ._rendering_queue . put ,
399
+ self ._schedule_render_task ,
419
400
)
420
401
else :
421
402
new_child_state = _update_component_model_state (
422
403
old_child_state ,
423
404
new_state ,
424
405
index ,
425
406
child ,
426
- self ._rendering_queue . put ,
407
+ self ._schedule_render_task ,
427
408
)
428
409
await self ._render_component (
429
410
exit_stack , old_child_state , new_child_state , child
@@ -458,7 +439,7 @@ async def _render_model_children_without_old_state(
458
439
new_state .children_by_key [key ] = child_state
459
440
elif child_type is _COMPONENT_TYPE :
460
441
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
462
443
)
463
444
await self ._render_component (exit_stack , None , child_state , child )
464
445
else :
@@ -479,6 +460,21 @@ async def _unmount_model_states(self, old_states: list[_ModelState]) -> None:
479
460
480
461
to_unmount .extend (model_state .children_by_key .values ())
481
462
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
+
482
478
def __repr__ (self ) -> str :
483
479
return f"{ type (self ).__name__ } ({ self .root } )"
484
480
0 commit comments