Add live reload to JupyUvi#875
Conversation
a2c6a43 to
abe65f1
Compare
|
Can you add a bit of an explanation/example to this @curtis-allan ? |
abe65f1 to
eb68c70
Compare
| class JupyUvi: | ||
| "Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`" | ||
| def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, daemon=False, **kwargs): | ||
| def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, live=False, live_rt='/_lr', daemon=False, **kwargs): |
There was a problem hiding this comment.
The live_rt param is the path of the SSE route that get's registered in the app if live is set to True, to send reload events to connected clients
| store_attr(but='start,live') | ||
| self.server = None | ||
| self._live_ver = 0 | ||
| if live: self._setup_live(app) |
There was a problem hiding this comment.
Since multiple clients can be connected to our app at a given time, using a version over a boolean flag ensures all client's reload (since each get's their own version number once connected to the generator endpoint)
| def _setup_live(self, app): | ||
| rt = self.live_rt or '/_lr' | ||
| if not rt.startswith('/'): rt = f'/{rt}' | ||
| app.hdrs.append(Script(f"new EventSource({rt!r}).onmessage=e=>{{if(e.data==='reload')navigation.reload()}}")) |
There was a problem hiding this comment.
Registers a simple EventSource in the client browsers by adding it to the global app headers. Logic is as follows:
- Clients connect to an SSE endpoint at
self.live_rt(which falls back to/_lrif overridden) - When the browser receives a
data: reloadevent from the server, it triggers a browser-side reload to fetch the latest version of whatever page is loaded
| app.hdrs.append(Script(f"new EventSource({rt!r}).onmessage=e=>{{if(e.data==='reload')navigation.reload()}}")) | ||
| @app.get(rt) | ||
| async def _sse(): return EventStream(self._live_sse()) | ||
| get_ipython().events.register('post_run_cell', lambda _: setattr(self, '_live_ver', self._live_ver+1)) |
There was a problem hiding this comment.
This registers a global callback in the running IPykernel. Each time a cell has been run/ executed in the kernel, it triggers the callback & increments the self._live_ver variable
| await asyncio.sleep(0.1) | ||
| if ver != self._live_ver: | ||
| ver = self._live_ver | ||
| yield 'data: reload\n\n' |
There was a problem hiding this comment.
The generator which sends the reload events to connected clients.
Each client that connects receives it's own new generator on the server-side. Each generator stores an initial reference to the JupyUvi _live_ver variable.
If the client's version doesn't match the server version state, it sends a data: reload event to that client, triggering a browser refresh.
The self.server.should_exit loop conditional ensures generators are stopped & cleaned up before the uvicorn thread/ process exits so it doesn't cause connection hangs. It seems to work well in notebook environments since stop() sets the server attribute first
|
@jph00 I've recorded a demo of the behaviour this PR aims to add, and I've annotated the code changes to indicate my reasoning behind them |
|
Thanks @curtis-allan this is great! :) Those comments might be better as comments in the code, docstring, or ipynb, for future readers. Should we replace the fastapp live reload with this impl? Would be nice to share code if possible... |
That makes sense @jph00 , I'll document the core parts in the notebook and re-push. I haven't looked at the fastapp live-reload logic in a while but that would be nice. It wouldn't work OOTB as a drop-in replacement though, since running a Will have a look and see |
This PR add live browser reload functionality to JupyUvi through a
livekeyword arg.Since
JupyUviruns uvicorn in a separate thread, the code is automatically updated when changed within the kernel namespace. Thefast_applife refresh websocket logic listens for a disconnection to trigger the browser reload, but withJupyUvithe server process doesn't need to restart to update the app logic.By using a simple
sseendpoint and binding the reload flag callback to:get_ipython().events.register('post_run_cell', ...We can initiate a browser refresh after a cell has been run in the kernel, which makes for a smooth dev experience in notebook-like contexts.