@@ -1205,6 +1205,11 @@ class ComponentContext:
12051205 # the render phase skipped this whole subtree (nothing changed), so the
12061206 # reconciliation phase can reuse the previous result without walking
12071207 clean_subtree : bool = False
1208+ # Fast renderer only: stale component effect cleanups are run at the same
1209+ # point where the default renderer removes stale elements, while widget
1210+ # closing stays deferred to the fast renderer's stale sweep.
1211+ fast_stale_effect_keys : Optional [List [str ]] = None
1212+ fast_stale_effects_cleaned : Set [str ] = field (default_factory = set )
12081213
12091214 # elements created in this context go there
12101215 owns : Set [Element ] = field (default_factory = set )
@@ -1263,6 +1268,8 @@ def _teardown_component_context(context: ComponentContext):
12631268 context .exceptions_self = []
12641269 context .exceptions_children = []
12651270 context .context_managers = []
1271+ context .fast_stale_effect_keys = None
1272+ context .fast_stale_effects_cleaned = set ()
12661273
12671274
12681275@dataclass
@@ -2523,6 +2530,8 @@ def _render(self, element: Element, default_key: str, parent_key: str):
25232530 # the root element of a component determines which keys are in use,
25242531 # everything else is stale and gets removed during reconciliation
25252532 context .used_keys .clear ()
2533+ context .fast_stale_effect_keys = None
2534+ context .fast_stale_effects_cleaned .clear ()
25262535
25272536 el = element
25282537 key = el ._key
@@ -2923,6 +2932,7 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str):
29232932 self ._shared_elements .add (el )
29242933 assert el in self ._shared_elements_next
29252934 self ._shared_elements_next .remove (el )
2935+ self ._cleanup_stale_effects_for_context (context , parent_key )
29262936
29272937 def _process_effects (self , child_context : "ComponentContext" , context : "ComponentContext" ):
29282938 # NOTE: effect/cleanup exceptions are recorded on the context of the
@@ -2965,6 +2975,64 @@ def _store_widget(self, context: "ComponentContext", el: Element, key: str, widg
29652975 else :
29662976 context .widgets [key ] = widget
29672977
2978+ def _cleanup_stale_effects_for_context (self , context : "ComponentContext" , parent_key : str ):
2979+ if context .fast_stale_effect_keys is None :
2980+ # reversed, so we can pop from the end and still go in sorted order
2981+ context .fast_stale_effect_keys = sorted (set (context .elements ) - context .used_keys , reverse = True )
2982+ context_prev = self .context
2983+ try :
2984+ while context .fast_stale_effect_keys :
2985+ stale_key = context .fast_stale_effect_keys .pop ()
2986+ if stale_key not in context .elements or stale_key in context .fast_stale_effects_cleaned :
2987+ continue
2988+ self .context = context
2989+ self ._cleanup_stale_effects (context .elements [stale_key ], stale_key , parent_key )
2990+ finally :
2991+ self .context = context_prev
2992+
2993+ def _cleanup_stale_effects (self , el : Element , default_key : str , parent_key : str ):
2994+ key = el ._key
2995+ if key is None :
2996+ key = default_key
2997+ assert key is not None
2998+ context = self .context
2999+ assert context is not None
3000+ if key in context .fast_stale_effects_cleaned :
3001+ return
3002+ if el .is_shared and (el in self ._shared_elements_next or el not in self ._shared_elements ):
3003+ return
3004+ context .fast_stale_effects_cleaned .add (key )
3005+
3006+ if isinstance (el .component , ComponentFunction ):
3007+ if el .is_shared :
3008+ self ._visit_children (el , key , parent_key , self ._cleanup_stale_effects )
3009+ child_context = context .children .get (key )
3010+ if child_context is None :
3011+ return
3012+ try :
3013+ self .context = child_context
3014+ child_context .exceptions_self = []
3015+ child_context .exceptions_children = []
3016+ for effect in child_context .effects :
3017+ try :
3018+ if not effect ._cleaned_up :
3019+ effect .cleanup ()
3020+ except BaseException as e :
3021+ effect ._cleaned_up = True
3022+ logger .exception ("Effect cleanup %r raised exception %r" , effect .callable , e )
3023+ child_context .exceptions_self .append (e )
3024+ self ._set_rerender_needed ("Exception ocurred during effect" )
3025+ _mark_needs_render_ancestors (child_context )
3026+ assert child_context .root_element is not None
3027+ self ._cleanup_stale_effects (child_context .root_element , "/" , parent_key = join_key (parent_key , key ))
3028+ finally :
3029+ self .context = context
3030+ if child_context .exceptions_self or child_context .exceptions_children and not child_context .exception_handler :
3031+ context .exceptions_children .extend (child_context .exceptions_self )
3032+ context .exceptions_children .extend (child_context .exceptions_children )
3033+ else :
3034+ self ._visit_children (el , key , parent_key , self ._cleanup_stale_effects )
3035+
29683036 def _remove_element (self , el : Element , default_key : str , parent_key ):
29693037 key = el ._key
29703038 if key is None :
0 commit comments