Skip to content

Commit

Permalink
calc response diffs (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogrodnek authored Sep 2, 2024
1 parent bd32301 commit 8f1aae7
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 6 deletions.
34 changes: 34 additions & 0 deletions pyview/template/render_diff.py
Original file line number Diff line number Diff line change
@@ -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
25 changes: 19 additions & 6 deletions pyview/ws_handler.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -105,14 +108,17 @@ 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 = [
joinRef,
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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Empty file added tests/template/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions tests/template/test_diff.py
Original file line number Diff line number Diff line change
@@ -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("<div>{% if greeting %}<span>{{greeting}}</span>{% endif %}</div>")
assert calc_diff(t.tree({"greeting": "Hello"}), t.tree({"greeting": "Hello"})) == {}


def test_simple_diff():
t = Template("<div>{% if greeting %}<span>{{greeting}}</span>{% endif %}</div>")

hello = t.tree({"greeting": "Hello"})
goodbye = t.tree({"greeting": "Goodbye"})

assert calc_diff(hello, goodbye) == {"0": {"0": "Goodbye"}}


def test_conditional_diff():
t = Template("<div>{% if greeting %}<span>{{greeting}}</span>{% endif %}</div>")

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": ["<span>", "</span>"], "0": "Hello"}}


def test_loop_diff():
t = Template("<div>{% for item in items %}<span>{{item}}</span>{% endfor %}</div>")

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("<div>{% for item in items %}<span>{{item}}</span>{% endfor %}</div>")

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("<div>{% for item in items %}<span>{{item}}</span>{% endfor %}</div>")
t2 = Template("<div>{% for item in items %}<div>{{item}}</div>{% endfor %}</div>")

old = t.tree({"items": ["One", "Two", "Three"]})
new = t2.tree({"items": ["One", "Two", "Three", "Four"]})

assert calc_diff(old, new) == {
"0": {"s": ["<div>", "</div>"], "d": [["One"], ["Two"], ["Three"], ["Four"]]}
}


def test_diff_template_change():
t = Template("<div><span>{{greeting}}</span></div>")
t2 = Template(
"<div>{% if greeting %}<span>{{greeting}}</span>{% endif %}<p>{{farewell}}</p></div>"
)

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("<div><span>{{greeting}}</span></div>")
t2 = Template("<div>{{greeting}}</div>")

r1 = t.tree({"greeting": "Hello"})
r2 = t2.tree({"greeting": "Hello"})

assert calc_diff(r1, r2) == {"s": ["<div>", "</div>"]}

0 comments on commit 8f1aae7

Please sign in to comment.