diff --git a/pyview/template/render_diff.py b/pyview/template/render_diff.py new file mode 100644 index 0000000..e5d6800 --- /dev/null +++ b/pyview/template/render_diff.py @@ -0,0 +1,34 @@ +from typing import Any + + +def calc_diff(old_tree: dict[str, Any], new_tree: dict[str, Any]) -> dict[str, Any]: + diff = {} + for key in new_tree: + if key not in old_tree: + diff[key] = new_tree[key] + elif ( + isinstance(new_tree[key], dict) + and "s" in new_tree[key] + and "d" in new_tree[key] + ): + # Handle special case of for loop + old_dynamic = old_tree[key]["d"] + new_dynamic = new_tree[key]["d"] + + old_static = old_tree[key]["s"] + new_static = new_tree[key]["s"] + + if old_static != new_static: + diff[key] = {"s": new_static, "d": new_dynamic} + continue + + if old_dynamic != new_dynamic: + diff[key] = {"d": new_dynamic} + elif isinstance(new_tree[key], dict): + nested_diff = calc_diff(old_tree[key], new_tree[key]) + if nested_diff: + diff[key] = nested_diff + elif old_tree[key] != new_tree[key]: + diff[key] = new_tree[key] + + return diff diff --git a/pyview/ws_handler.py b/pyview/ws_handler.py index 40e1908..d582e88 100644 --- a/pyview/ws_handler.py +++ b/pyview/ws_handler.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Any import json from starlette.websockets import WebSocket, WebSocketDisconnect from urllib.parse import urlparse, parse_qs @@ -8,6 +8,7 @@ from pyview.session import deserialize_session from pyview.auth import AuthProviderFactory from pyview.phx_message import parse_message +from pyview.template.render_diff import calc_diff class AuthException(Exception): @@ -63,7 +64,7 @@ async def handle(self, websocket: WebSocket): ] await self.manager.send_personal_message(json.dumps(resp), websocket) - await self.handle_connected(topic, socket) + await self.handle_connected(topic, socket, rendered) except WebSocketDisconnect: if socket: @@ -73,7 +74,9 @@ async def handle(self, websocket: WebSocket): await websocket.close() self.sessions -= 1 - async def handle_connected(self, myJoinId, socket: LiveViewSocket): + async def handle_connected( + self, myJoinId, socket: LiveViewSocket, prev_rendered: dict[str, Any] + ): while True: message = await socket.websocket.receive() [joinRef, mesageRef, topic, event, payload] = parse_message(message) @@ -105,6 +108,9 @@ async def handle_connected(self, myJoinId, socket: LiveViewSocket): {} if not socket.pending_events else {"e": socket.pending_events} ) + diff = calc_diff(prev_rendered, rendered) + prev_rendered = rendered + socket.pending_events = [] resp = [ @@ -112,7 +118,7 @@ async def handle_connected(self, myJoinId, socket: LiveViewSocket): mesageRef, topic, "phx_reply", - {"response": {"diff": rendered | hook_events}, "status": "ok"}, + {"response": {"diff": diff | hook_events}, "status": "ok"}, ] await self.manager.send_personal_message( json.dumps(resp), socket.websocket @@ -125,13 +131,15 @@ async def handle_connected(self, myJoinId, socket: LiveViewSocket): await lv.handle_params(url, parse_qs(url.query), socket) rendered = await _render(socket) + diff = calc_diff(prev_rendered, rendered) + prev_rendered = rendered resp = [ joinRef, mesageRef, topic, "phx_reply", - {"response": {"diff": rendered}, "status": "ok"}, + {"response": {"diff": diff}, "status": "ok"}, ] await self.manager.send_personal_message( json.dumps(resp), socket.websocket @@ -142,7 +150,10 @@ async def handle_connected(self, myJoinId, socket: LiveViewSocket): allow_upload_response = socket.upload_manager.process_allow_upload( payload ) + rendered = await _render(socket) + diff = calc_diff(prev_rendered, rendered) + prev_rendered = rendered resp = [ joinRef, @@ -208,13 +219,15 @@ async def handle_connected(self, myJoinId, socket: LiveViewSocket): if event == "progress": socket.upload_manager.update_progress(joinRef, payload) rendered = await _render(socket) + diff = calc_diff(prev_rendered, rendered) + prev_rendered = rendered resp = [ joinRef, mesageRef, topic, "phx_reply", - {"response": {"diff": rendered}, "status": "ok"}, + {"response": {"diff": diff}, "status": "ok"}, ] await self.manager.send_personal_message( diff --git a/tests/template/__init__.py b/tests/template/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/template/test_diff.py b/tests/template/test_diff.py new file mode 100644 index 0000000..ed7f751 --- /dev/null +++ b/tests/template/test_diff.py @@ -0,0 +1,83 @@ +from pyview.vendor.ibis import Template +from pyview.template.render_diff import calc_diff + + +def test_simple_diff_no_changes(): + t = Template("
{{farewell}}