10
10
from asyncio import Queue as AsyncQueue
11
11
from asyncio .futures import Future
12
12
from threading import Event as ThreadEvent
13
- from typing import Any , Dict , List , Optional , Tuple , Type , Union
13
+ from typing import Any , List , Optional , Tuple , Type , Union
14
14
from urllib .parse import urljoin
15
15
16
16
from tornado .platform .asyncio import AsyncIOMainLoop
23
23
from idom .core .dispatcher import dispatch_single_view
24
24
from idom .core .layout import Layout , LayoutEvent , LayoutUpdate
25
25
26
- from .base import AbstractRenderServer
26
+ from .utils import threaded , wait_on_event
27
27
28
28
29
29
_RouteHandlerSpecs = List [Tuple [str , Type [RequestHandler ], Any ]]
32
32
class Config (TypedDict , total = False ):
33
33
"""Render server config for :class:`TornadoRenderServer` subclasses"""
34
34
35
- base_url : str
35
+ url_prefix : str
36
36
serve_static_files : bool
37
37
redirect_root_to_index : bool
38
38
39
39
40
- class TornadoRenderServer (AbstractRenderServer [Application , Config ]):
41
- """A base class for all Tornado render servers"""
40
+ def PerClientStateServer (
41
+ constructor : ComponentConstructor ,
42
+ config : Optional [Config ] = None ,
43
+ app : Optional [Application ] = None ,
44
+ ) -> TornadoServer :
45
+ """Return a :class:`FastApiServer` where each client has its own state.
42
46
43
- _model_stream_handler_type : Type [ WebSocketHandler ]
47
+ Implements the :class:`~idom.server.proto.ServerFactory` protocol
44
48
45
- def stop (self , timeout : Optional [float ] = None ) -> None :
49
+ Parameters:
50
+ constructor: A component constructor
51
+ config: Options for configuring server behavior
52
+ app: An application instance (otherwise a default instance is created)
53
+ """
54
+ config , app = _setup_config_and_app (config , app )
55
+ _add_handler (
56
+ app ,
57
+ config ,
58
+ _setup_common_routes (config ) + _setup_single_view_dispatcher_route (constructor ),
59
+ )
60
+ return TornadoServer (app )
61
+
62
+
63
+ class TornadoServer :
64
+ """A thin wrapper for running a Tornado application
65
+
66
+ See :class:`idom.server.proto.Server` for more info
67
+ """
68
+
69
+ _loop : asyncio .AbstractEventLoop
70
+
71
+ def __init__ (self , app : Application ) -> None :
72
+ self .app = app
73
+ self ._did_start = ThreadEvent ()
74
+
75
+ def run (self , host : str , port : int , * args : Any , ** kwargs : Any ) -> None :
76
+ self ._loop = asyncio .get_event_loop ()
77
+ AsyncIOMainLoop ().install ()
78
+ self .app .listen (port , host , * args , ** kwargs )
79
+ self ._did_start .set ()
80
+ asyncio .get_event_loop ().run_forever ()
81
+
82
+ @threaded
83
+ def run_in_thread (self , host : str , port : int , * args : Any , ** kwargs : Any ) -> None :
84
+ self .run (host , port , * args , ** kwargs )
85
+
86
+ def wait_until_started (self , timeout : Optional [float ] = 3.0 ) -> None :
87
+ self ._did_start .wait (timeout )
88
+
89
+ def stop (self , timeout : Optional [float ] = 3.0 ) -> None :
46
90
try :
47
91
loop = self ._loop
48
92
except AttributeError : # pragma: no cover
@@ -57,87 +101,61 @@ def stop() -> None:
57
101
did_stop .set ()
58
102
59
103
loop .call_soon_threadsafe (stop )
60
- did_stop .wait (timeout )
61
104
62
- def _create_config (self , config : Optional [Config ]) -> Config :
63
- new_config : Config = {
64
- "base_url" : "" ,
105
+ wait_on_event (f"stop { self .app } " , did_stop , timeout )
106
+
107
+
108
+ def _setup_config_and_app (
109
+ config : Optional [Config ], app : Optional [Application ]
110
+ ) -> Tuple [Config , Application ]:
111
+ return (
112
+ {
113
+ "url_prefix" : "" ,
65
114
"serve_static_files" : True ,
66
115
"redirect_root_to_index" : True ,
67
116
** (config or {}), # type: ignore
68
- }
69
- return new_config
70
-
71
- def _default_application (self , config : Config ) -> Application :
72
- return Application ()
73
-
74
- def _setup_application (
75
- self ,
76
- config : Config ,
77
- app : Application ,
78
- ) -> None :
79
- base_url = config ["base_url" ]
80
- app .add_handlers (
81
- r".*" ,
82
- [
83
- (urljoin (base_url , route_pattern ),) + tuple (handler_info ) # type: ignore
84
- for route_pattern , * handler_info in self ._create_route_handlers (config )
85
- ],
86
- )
117
+ },
118
+ app or Application (),
119
+ )
87
120
88
- def _setup_application_did_start_event (
89
- self , config : Config , app : Application , event : ThreadEvent
90
- ) -> None :
91
- pass
92
121
93
- def _create_route_handlers (self , config : Config ) -> _RouteHandlerSpecs :
94
- handlers : _RouteHandlerSpecs = [
122
+ def _setup_common_routes (config : Config ) -> _RouteHandlerSpecs :
123
+ handlers : _RouteHandlerSpecs = []
124
+ if config ["serve_static_files" ]:
125
+ handlers .append (
95
126
(
96
- "/stream" ,
97
- self ._model_stream_handler_type ,
98
- {"component_constructor" : self ._root_component_constructor },
99
- )
100
- ]
101
-
102
- if config ["serve_static_files" ]:
103
- handlers .append (
104
- (
105
- r"/client/(.*)" ,
106
- StaticFileHandler ,
107
- {"path" : str (IDOM_CLIENT_BUILD_DIR .current )},
108
- )
127
+ r"/client/(.*)" ,
128
+ StaticFileHandler ,
129
+ {"path" : str (IDOM_CLIENT_BUILD_DIR .current )},
109
130
)
110
- if config ["redirect_root_to_index" ]:
111
- handlers .append (("/" , RedirectHandler , {"url" : "./client/index.html" }))
112
-
113
- return handlers
114
-
115
- def _run_application (
116
- self ,
117
- config : Config ,
118
- app : Application ,
119
- host : str ,
120
- port : int ,
121
- args : Tuple [Any , ...],
122
- kwargs : Dict [str , Any ],
123
- ) -> None :
124
- self ._loop = asyncio .get_event_loop ()
125
- AsyncIOMainLoop ().install ()
126
- app .listen (port , host , * args , ** kwargs )
127
- self ._server_did_start .set ()
128
- asyncio .get_event_loop ().run_forever ()
129
-
130
- def _run_application_in_thread (
131
- self ,
132
- config : Config ,
133
- app : Application ,
134
- host : str ,
135
- port : int ,
136
- args : Tuple [Any , ...],
137
- kwargs : Dict [str , Any ],
138
- ) -> None :
139
- asyncio .set_event_loop (asyncio .new_event_loop ())
140
- self ._run_application (config , app , host , port , args , kwargs )
131
+ )
132
+ if config ["redirect_root_to_index" ]:
133
+ handlers .append (("/" , RedirectHandler , {"url" : "./client/index.html" }))
134
+ return handlers
135
+
136
+
137
+ def _add_handler (
138
+ app : Application , config : Config , handlers : _RouteHandlerSpecs
139
+ ) -> None :
140
+ app .add_handlers (
141
+ r".*" ,
142
+ [
143
+ (urljoin (config ["url_prefix" ], route_pattern ),) + tuple (handler_info )
144
+ for route_pattern , * handler_info in handlers
145
+ ],
146
+ )
147
+
148
+
149
+ def _setup_single_view_dispatcher_route (
150
+ constructor : ComponentConstructor ,
151
+ ) -> _RouteHandlerSpecs :
152
+ return [
153
+ (
154
+ "/stream" ,
155
+ PerClientStateModelStreamHandler ,
156
+ {"component_constructor" : constructor },
157
+ )
158
+ ]
141
159
142
160
143
161
class PerClientStateModelStreamHandler (WebSocketHandler ):
@@ -176,9 +194,3 @@ async def on_message(self, message: Union[str, bytes]) -> None:
176
194
def on_close (self ) -> None :
177
195
if not self ._dispatch_future .done ():
178
196
self ._dispatch_future .cancel ()
179
-
180
-
181
- class PerClientStateServer (TornadoRenderServer ):
182
- """Each client view will have its own state."""
183
-
184
- _model_stream_handler_type = PerClientStateModelStreamHandler
0 commit comments