55"""
66
77from __future__ import annotations
8+ import threading
9+ import time
810from dataclasses import dataclass , field
911from typing import Any , Callable , Generic , Literal , Optional , TypeVar , Union
1012
@@ -49,6 +51,7 @@ class RuntimeState:
4951 current_update_priority : UpdatePriority = "default"
5052 pending_update_priority : Optional [UpdatePriority ] = None
5153 after_batch_callbacks : list [Callable [[], None ]] = field (default_factory = list )
54+ scheduled_rerender : bool = False
5255
5356
5457@dataclass
@@ -59,6 +62,59 @@ class Ref(Generic[T]):
5962_runtime = RuntimeState ()
6063_rerender_callback : Optional [Callable [[], None ]] = None
6164_UNSET = object ()
65+ _scheduler_lock = threading .Lock ()
66+ _scheduler_event = threading .Event ()
67+ _scheduler_thread : Optional [threading .Thread ] = None
68+
69+
70+ def _ensure_scheduler_thread () -> None :
71+ global _scheduler_thread
72+
73+ with _scheduler_lock :
74+ if _scheduler_thread is not None and _scheduler_thread .is_alive ():
75+ return
76+
77+ thread = threading .Thread (
78+ target = _scheduled_rerender_loop ,
79+ name = "pyinkcli-hooks-rerender" ,
80+ daemon = True ,
81+ )
82+ thread .start ()
83+ _scheduler_thread = thread
84+
85+
86+ def _scheduled_rerender_loop () -> None :
87+ while True :
88+ _scheduler_event .wait ()
89+ _scheduler_event .clear ()
90+ # Match JS-style end-of-tick batching: defer dispatch until the current
91+ # Python thread yields so adjacent setter calls collapse into one render.
92+ time .sleep (0 )
93+ _flush_scheduled_rerender ()
94+
95+
96+ def _schedule_scheduled_rerender () -> None :
97+ _ensure_scheduler_thread ()
98+ with _scheduler_lock :
99+ if _runtime .scheduled_rerender :
100+ return
101+ _runtime .scheduled_rerender = True
102+ _scheduler_event .set ()
103+
104+
105+ def _flush_scheduled_rerender () -> bool :
106+ callback : Optional [Callable [[], None ]]
107+ with _scheduler_lock :
108+ if not _runtime .scheduled_rerender :
109+ return False
110+ _runtime .scheduled_rerender = False
111+ callback = _rerender_callback
112+
113+ if callback is None :
114+ return False
115+
116+ callback ()
117+ return True
62118
63119
64120def _has_initialized_state_slot (state : HookState , hook_index : int ) -> bool :
@@ -273,11 +329,17 @@ def _clear_hook_state() -> None:
273329 _runtime .current_update_priority = "default"
274330 _runtime .pending_update_priority = None
275331 _runtime .after_batch_callbacks .clear ()
332+ _runtime .scheduled_rerender = False
333+ _scheduler_event .clear ()
276334
277335
278336def _set_rerender_callback (callback : Optional [Callable [[], None ]]) -> None :
279337 global _rerender_callback
280338 _rerender_callback = callback
339+ if callback is None :
340+ with _scheduler_lock :
341+ _runtime .scheduled_rerender = False
342+ _scheduler_event .clear ()
281343
282344
283345def _priority_rank (priority : UpdatePriority ) -> int :
@@ -422,7 +484,7 @@ def _request_rerender() -> None:
422484 return
423485
424486 if _rerender_callback is not None :
425- _rerender_callback ()
487+ _schedule_scheduled_rerender ()
426488
427489
428490def _flush_batched_rerender () -> None :
0 commit comments