Skip to content

Commit 89a551c

Browse files
committed
refine routing + fix bugs
1 parent b0696f1 commit 89a551c

File tree

8 files changed

+133
-154
lines changed

8 files changed

+133
-154
lines changed

docs/source/guides/getting-started/_static/embed-idom-view/index.html

+12-23
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,20 @@ <h1>This is an Example App</h1>
99
<p>Just below is an embedded IDOM view...</p>
1010
<div id="idom-app" />
1111
<script type="module">
12-
import { mountLayoutWithWebSocket } from "https://esm.sh/[email protected]";
12+
import {
13+
mountWithLayoutServer,
14+
LayoutServerInfo,
15+
} from "https://esm.sh/[email protected]";
1316

14-
// get the correct web socket protocol
15-
let wsURL;
16-
if (window.location.protocol === "https:") {
17-
wsURL = "wss:";
18-
} else {
19-
wsURL = "ws:";
20-
}
21-
wsURL += "//" + window.location.host;
17+
const serverInfo = new LayoutServerInfo({
18+
host: document.location.hostname,
19+
port: document.location.port,
20+
path: "_idom",
21+
query: queryParams.user.toString(),
22+
secure: document.location.protocol == "https:",
23+
});
2224

23-
// append path to the web socket
24-
wsURL += "/_idom/stream";
25-
26-
// only needed for views that use web modules
27-
const loadImportSource = (source, sourceType) =>
28-
sourceType == "NAME"
29-
? import("_idom/_modules/" + source)
30-
: import(source);
31-
32-
mountLayoutWithWebSocket(
33-
document.getElementById("idom-app"),
34-
wsURL,
35-
loadImportSource
36-
);
25+
mountLayoutWithWebSocket(document.getElementById("idom-app"), serverInfo);
3726
</script>
3827
</body>
3928
</html>

src/client/packages/idom-client-react/src/server.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ export function mountWithLayoutServer(
2222
);
2323
}
2424

25-
export function LayoutServerInfo({ host, port, query, secure }) {
25+
export function LayoutServerInfo({ host, port, path, query, secure }) {
2626
const wsProtocol = "ws" + (secure ? "s" : "");
2727
const httpProtocol = "http" + (secure ? "s" : "");
2828

29-
const uri = host + ":" + port;
30-
const path = new URL(document.baseURI).pathname;
31-
const url = uri + path;
29+
let url = host + ":" + port + (path || new URL(document.baseURI).pathname);
30+
31+
if (url.endsWith("/")) {
32+
url = url.slice(0, -1);
33+
}
3234

3335
const wsBaseUrl = wsProtocol + "://" + url;
3436
const httpBaseUrl = httpProtocol + "://" + url;
@@ -40,7 +42,7 @@ export function LayoutServerInfo({ host, port, query, secure }) {
4042
}
4143

4244
this.path = {
43-
stream: wsBaseUrl + "/_stream" + query,
44-
module: (source) => httpBaseUrl + `/modules/${source}`,
45+
stream: wsBaseUrl + "/_api/stream" + query,
46+
module: (source) => httpBaseUrl + `/_api/modules/${source}`,
4547
};
4648
}

src/idom/server/flask.py

+55-47
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,30 @@
1616
Flask,
1717
Request,
1818
copy_current_request_context,
19-
redirect,
2019
request,
2120
send_from_directory,
22-
url_for,
2321
)
2422
from flask_cors import CORS
2523
from flask_sock import Sock
26-
from gevent import pywsgi
27-
from geventwebsocket.handler import WebSocketHandler
2824
from simple_websocket import Server as WebSocket
25+
from werkzeug.serving import BaseWSGIServer, make_server
2926

3027
import idom
3128
from idom.config import IDOM_WEB_MODULES_DIR
3229
from idom.core.hooks import Context, create_context, use_context
3330
from idom.core.layout import LayoutEvent, LayoutUpdate
3431
from idom.core.serve import serve_json_patch
3532
from idom.core.types import ComponentType, RootComponentConstructor
33+
from idom.utils import Ref
3634

3735
from .utils import CLIENT_BUILD_DIR, client_build_dir_path
3836

3937

4038
logger = logging.getLogger(__name__)
4139

42-
RequestContext: type[Context[Request | None]] = create_context(None, "RequestContext")
40+
ConnectionContext: type[Context[Connection | None]] = create_context(
41+
None, "ConnectionContext"
42+
)
4343

4444

4545
def configure(
@@ -57,10 +57,10 @@ def configure(
5757
options = options or Options()
5858
blueprint = Blueprint("idom", __name__, url_prefix=options.url_prefix)
5959

60-
_setup_common_routes(blueprint, options)
61-
6260
# this route should take priority so set up it up first
63-
_setup_single_view_dispatcher_route(app, options, component)
61+
_setup_single_view_dispatcher_route(blueprint, options, component)
62+
63+
_setup_common_routes(blueprint, options)
6464

6565
app.register_blueprint(blueprint)
6666

@@ -82,21 +82,15 @@ async def serve_development_app(
8282
loop = asyncio.get_event_loop()
8383
stopped = asyncio.Event()
8484

85-
server: pywsgi.WSGIServer
85+
server: Ref[BaseWSGIServer] = Ref()
8686

8787
def run_server() -> None: # pragma: no cover
8888
# we don't cover this function because coverage doesn't work right in threads
89-
nonlocal server
90-
server = pywsgi.WSGIServer(
91-
(host, port),
92-
app,
93-
handler_class=WebSocketHandler,
94-
)
95-
server.start()
89+
server.current = make_server(host, port, app, threaded=True)
9690
if started:
9791
loop.call_soon_threadsafe(started.set)
9892
try:
99-
server.serve_forever()
93+
server.current.serve_forever()
10094
finally:
10195
loop.call_soon_threadsafe(stopped.set)
10296

@@ -110,27 +104,27 @@ def run_server() -> None: # pragma: no cover
110104
await stopped.wait()
111105
finally:
112106
# we may have exitted because this task was cancelled
113-
server.stop(3)
107+
server.current.shutdown()
114108
# the thread should eventually join
115109
thread.join(timeout=3)
116110
# just double check it happened
117111
if thread.is_alive(): # pragma: no cover
118112
raise RuntimeError("Failed to shutdown server.")
119113

120114

121-
def use_request() -> Request:
122-
"""Get the current ``Request``"""
123-
request = use_context(RequestContext)
124-
if request is None:
115+
def use_connection() -> Connection:
116+
"""Get the current :class:`Connection`"""
117+
connection = use_context(ConnectionContext)
118+
if connection is None:
125119
raise RuntimeError( # pragma: no cover
126-
"No request. Are you running with a Flask server?"
120+
"No connection. Are you running with a Flask server?"
127121
)
128-
return request
122+
return connection
129123

130124

131125
def use_scope() -> dict[str, Any]:
132126
"""Get the current WSGI environment"""
133-
return use_request().environ
127+
return use_connection().request.environ
134128

135129

136130
@dataclass
@@ -143,9 +137,6 @@ class Options:
143137
For more information see docs for ``flask_cors.CORS``
144138
"""
145139

146-
redirect_root: bool = True
147-
"""Whether to redirect the root URL (with prefix) to ``index.html``"""
148-
149140
serve_static_files: bool = True
150141
"""Whether or not to serve static files (i.e. web modules)"""
151142

@@ -161,35 +152,29 @@ def _setup_common_routes(blueprint: Blueprint, options: Options) -> None:
161152

162153
if options.serve_static_files:
163154

164-
@blueprint.route("/app")
165-
@blueprint.route("/app<path:path>")
155+
@blueprint.route("/")
156+
@blueprint.route("/<path:path>")
166157
def send_client_dir(path: str = "") -> Any:
167158
return send_from_directory(
168159
str(CLIENT_BUILD_DIR),
169160
client_build_dir_path(path),
170161
)
171162

172-
@blueprint.route("/modules/<path:path>")
173-
def send_modules_dir(path: str) -> Any:
163+
@blueprint.route(r"/_api/modules/<path:path>")
164+
@blueprint.route(r"<path:_>/_api/modules/<path:path>")
165+
def send_modules_dir(
166+
path: str,
167+
_: str = "", # this is not used
168+
) -> Any:
174169
return send_from_directory(str(IDOM_WEB_MODULES_DIR.current), path)
175170

176-
if options.redirect_root:
177-
178-
@blueprint.route("/")
179-
def redirect_to_index() -> Any:
180-
return redirect(
181-
url_for("idom.send_client_dir", path="", **request.args)
182-
)
183-
184171

185172
def _setup_single_view_dispatcher_route(
186-
app: Flask, options: Options, constructor: RootComponentConstructor
173+
blueprint: Blueprint, options: Options, constructor: RootComponentConstructor
187174
) -> None:
188-
sockets = Sock(app)
175+
sock = Sock(blueprint)
189176

190-
@sockets.route(_join_url_paths(options.url_prefix, "/app/_stream"))
191-
@sockets.route(_join_url_paths(options.url_prefix, "/app/<path:path>/_stream"))
192-
def model_stream(ws: WebSocket) -> None:
177+
def model_stream(ws: WebSocket, path: str = "") -> None:
193178
def send(value: Any) -> None:
194179
ws.send(json.dumps(value))
195180

@@ -200,10 +185,15 @@ def recv() -> Optional[LayoutEvent]:
200185
else:
201186
return None
202187

203-
dispatch_in_thread(constructor(), send, recv)
188+
dispatch_in_thread(ws, path, constructor(), send, recv)
189+
190+
sock.route("/_api/stream", endpoint="without_path")(model_stream)
191+
sock.route("/<path:path>/_api/stream", endpoint="with_path")(model_stream)
204192

205193

206194
def dispatch_in_thread(
195+
websocket: WebSocket,
196+
path: str,
207197
component: ComponentType,
208198
send: Callable[[Any], None],
209199
recv: Callable[[], Optional[LayoutEvent]],
@@ -227,7 +217,11 @@ async def recv_coro() -> Any:
227217

228218
async def main() -> None:
229219
await serve_json_patch(
230-
idom.Layout(RequestContext(component, value=request)),
220+
idom.Layout(
221+
ConnectionContext(
222+
component, value=Connection(request, websocket, path)
223+
)
224+
),
231225
send_coro,
232226
recv_coro,
233227
)
@@ -277,6 +271,20 @@ def run_send() -> None:
277271
return None
278272

279273

274+
@dataclass
275+
class Connection:
276+
"""A simple wrapper for holding connection information"""
277+
278+
request: Request
279+
"""The current request object"""
280+
281+
websocket: WebSocket
282+
"""A handle to the current websocket"""
283+
284+
path: str
285+
"""The current path being served"""
286+
287+
280288
class _DispatcherThreadInfo(NamedTuple):
281289
dispatch_loop: asyncio.AbstractEventLoop
282290
dispatch_future: "asyncio.Future[Any]"

0 commit comments

Comments
 (0)