77
88from fastapi import FastAPI
99from fastui import AnyComponent , FastUI
10- from pydantic import NonNegativeFloat , TypeAdapter
10+ from pydantic import NonNegativeFloat
1111from servicelib .fastapi .app_state import SingletonInAppStateMixin
1212
1313UpdateID : TypeAlias = int
1414
1515
1616class AbstractSSERenderer (ABC ):
17- def __init__ (self ) -> None :
17+ def __init__ (self , app : FastAPI ) -> None :
18+ self .app = app
1819 self ._items : list [Any ] = []
20+ self ._hash = self ._get_items_hash ()
21+
22+ async def __aenter__ (self ):
23+ await RendererManager .get_from_app_state (self .app ).register_renderer (self )
24+ return self
25+
26+ async def __aexit__ (self , * args ):
27+ await RendererManager .get_from_app_state (self .app ).unregister_renderer (
28+ type (self ), self
29+ )
30+
31+ def _get_items_hash (self ) -> int :
32+ return hash (json .dumps (self ._items ))
1933
2034 def update (self , items : list [Any ]) -> None :
2135 self ._items = items
36+ self ._hash = self ._get_items_hash ()
2237
2338 def _get_update_id (self ) -> UpdateID :
24- return hash ( json . dumps ( TypeAdapter ( list [ Any ]). validate_python ( self ._items )))
39+ return self ._hash
2540
2641 def changes_detected (self , last_update_id : UpdateID ) -> bool :
2742 return last_update_id != self ._get_update_id ()
@@ -40,48 +55,68 @@ class RendererManager(SingletonInAppStateMixin):
4055 """Allows to register SSE renderers and distribute data based on type"""
4156
4257 def __init__ (self ) -> None :
58+ self ._lock = asyncio .Lock ()
4359 self ._renderers : dict [
4460 type [AbstractSSERenderer ], WeakSet [AbstractSSERenderer ]
4561 ] = {}
4662
47- def register_renderer (self , renderer : AbstractSSERenderer ) -> None :
48- """NOTE: there is no reason to unregister anything due to WeakSet tracking"""
63+ async def register_renderer (self , renderer : AbstractSSERenderer ) -> None :
4964 renderer_type = type (renderer )
5065
5166 if renderer_type not in self ._renderers :
5267 self ._renderers [renderer_type ] = WeakSet ()
5368
54- self ._renderers [renderer_type ].add (renderer )
69+ async with self ._lock :
70+ self ._renderers [renderer_type ].add (renderer )
71+
72+ async def unregister_renderer (
73+ self , renderer_type : type [AbstractSSERenderer ], renderer : AbstractSSERenderer
74+ ) -> None :
75+ if renderer_type not in self ._renderers :
76+ pass
77+ async with self ._lock :
78+ self ._renderers [renderer_type ].remove (renderer )
5579
56- def update_renderer (
80+ async def update_renderers (
5781 self , renderer_type : type [AbstractSSERenderer ], items : list [Any ]
5882 ) -> None :
5983 """propagate updates to all instances of said type SSERenderer"""
60- for renderer in self ._renderers [renderer_type ]:
61- renderer .update (items )
84+ if renderer_type not in self ._renderers :
85+ return
86+
87+ async with self ._lock :
88+ for renderer in self ._renderers [renderer_type ]:
89+ renderer .update (items )
6290
6391
64- async def render_as_sse_items (
92+ async def render_items_on_change (
6593 app : FastAPI ,
6694 * ,
6795 renderer_type : type [AbstractSSERenderer ],
68- messages_check_interval : NonNegativeFloat = 3 ,
96+ messages_check_interval : NonNegativeFloat = 1 ,
6997) -> AsyncIterable [str ]:
7098 """used by the sse endpoint to render the content as it changes"""
7199
72- manager = RendererManager .get_from_app_state (app )
73- renderer = renderer_type ()
74- manager .register_renderer (renderer )
100+ async with renderer_type (app ) as renderer :
101+
102+ last_update_id , messages = renderer .get_messages ()
103+
104+ # Avoid the browser reconnecting
105+ while True :
106+ await asyncio .sleep (messages_check_interval )
107+
108+ update_id , messages = renderer .get_messages ()
109+
110+ if renderer .changes_detected (last_update_id = last_update_id ):
111+ yield f"data: { FastUI (root = messages ).model_dump_json (by_alias = True , exclude_none = True )} \n \n "
75112
76- update_id , messages = renderer . get_messages ()
113+ last_update_id = update_id
77114
78- # Avoid the browser reconnecting
79- while True :
80- if renderer .changes_detected (last_update_id = update_id ):
81- yield f"data: { FastUI (root = messages ).model_dump_json (by_alias = True , exclude_none = True )} \n \n "
82115
83- await asyncio .sleep (messages_check_interval )
84- update_id , messages = renderer .get_messages ()
116+ async def update_items (
117+ app : FastAPI , * , renderer_type : type [AbstractSSERenderer ], items : list [Any ]
118+ ) -> None :
119+ await RendererManager .get_from_app_state (app ).update_renderers (renderer_type , items )
85120
86121
87122def setup_sse (app : FastAPI ) -> None :
0 commit comments