From 98ded210daed41aa84df6e8da9a50c4c1cbaa74f Mon Sep 17 00:00:00 2001 From: Larry Ogrodnek Date: Sat, 17 Aug 2024 16:27:28 -0400 Subject: [PATCH] calc response diffs --- pyview/template/render_diff.py | 34 ++++++++++++++ pyview/ws_handler.py | 25 +++++++--- tests/template/__init__.py | 0 tests/template/test_diff.py | 83 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 pyview/template/render_diff.py create mode 100644 tests/template/__init__.py create mode 100644 tests/template/test_diff.py 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("
{% if greeting %}{{greeting}}{% endif %}
") + assert calc_diff(t.tree({"greeting": "Hello"}), t.tree({"greeting": "Hello"})) == {} + + +def test_simple_diff(): + t = Template("
{% if greeting %}{{greeting}}{% endif %}
") + + hello = t.tree({"greeting": "Hello"}) + goodbye = t.tree({"greeting": "Goodbye"}) + + assert calc_diff(hello, goodbye) == {"0": {"0": "Goodbye"}} + + +def test_conditional_diff(): + t = Template("
{% if greeting %}{{greeting}}{% endif %}
") + + hello = t.tree({"greeting": "Hello"}) + empty = t.tree({}) + + # Going from hello to empty + assert calc_diff(hello, empty) == {"0": ""} + + # Going from empty to hello + assert calc_diff(empty, hello) == {"0": {"s": ["", ""], "0": "Hello"}} + + +def test_loop_diff(): + t = Template("
{% for item in items %}{{item}}{% endfor %}
") + + old = t.tree({"items": ["One", "Two", "Three"]}) + new = t.tree({"items": ["One", "Two", "Four"]}) + + # diffs for loops always return all values, regardless of changes + # at least, I *think* that's the right behavior based on some qick liveview testing + assert calc_diff(old, new) == {"0": {"d": [["One"], ["Two"], ["Four"]]}} + + +def test_loop_diff_no_change(): + t = Template("
{% for item in items %}{{item}}{% endfor %}
") + + old = t.tree({"items": ["One", "Two", "Three"]}) + new = t.tree({"items": ["One", "Two", "Three"]}) + + assert calc_diff(old, new) == {} + + +def test_loop_diff_static_change(): + t = Template("
{% for item in items %}{{item}}{% endfor %}
") + t2 = Template("
{% for item in items %}
{{item}}
{% endfor %}
") + + old = t.tree({"items": ["One", "Two", "Three"]}) + new = t2.tree({"items": ["One", "Two", "Three", "Four"]}) + + assert calc_diff(old, new) == { + "0": {"s": ["
", "
"], "d": [["One"], ["Two"], ["Three"], ["Four"]]} + } + + +def test_diff_template_change(): + t = Template("
{{greeting}}
") + t2 = Template( + "
{% if greeting %}{{greeting}}{% endif %}

{{farewell}}

" + ) + + r1 = t.tree({"greeting": "Hello"}) + r2 = t2.tree({"greeting": "Hello", "farewell": "Goodbye"}) + + assert calc_diff(r1, r2) == r2 + + +def test_statics_only_change(): + t = Template("
{{greeting}}
") + t2 = Template("
{{greeting}}
") + + r1 = t.tree({"greeting": "Hello"}) + r2 = t2.tree({"greeting": "Hello"}) + + assert calc_diff(r1, r2) == {"s": ["
", "
"]}