Skip to content

Commit f911d42

Browse files
committed
Add server realtime
1 parent 396f8da commit f911d42

3 files changed

Lines changed: 149 additions & 4 deletions

File tree

examples/server_realtime.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from violetear import App, Document
2+
3+
app = App(title="Realtime Fire-and-Forget")
4+
5+
# 1. Server-Side Realtime Handler
6+
# Receives the message from the client (no return value sent back)
7+
@app.server.realtime
8+
async def log_on_server(msg: str):
9+
print(f"🔥 SERVER RECEIVED: {msg}")
10+
11+
# 2. Client-Side Callback
12+
# Must use @app.client.callback to be valid for .on() binding
13+
@app.client.callback
14+
async def on_click_ping(event):
15+
print("Client: Sending ping to server...")
16+
17+
# Fire-and-forget call to the server
18+
await log_on_server("Hello from the Browser!")
19+
20+
# 3. View with Fluent Syntax + Explicit Event Binding
21+
@app.view("/")
22+
def index():
23+
doc = Document(title="Realtime Test")
24+
25+
# Use the context manager to get the ElementBuilder 'e'
26+
with doc.body as e:
27+
e.h1("Realtime Test")
28+
e.p("Open your server terminal to see the logs.")
29+
30+
# Create the button and chain .on() to bind the event
31+
# This passes the function object 'on_click_ping' directly
32+
e.button("Send Ping").on("click", on_click_ping)
33+
34+
return doc
35+
36+
if __name__ == "__main__":
37+
app.run()

violetear/app.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,33 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str):
357357
await self.socket_manager.connect(client_id, websocket)
358358
try:
359359
while True:
360-
# Keep the connection alive.
361-
# We can also listen for client-to-server messages here if needed later.
362-
await websocket.receive()
360+
# Listen for messages from the client
361+
message = await websocket.receive_text()
362+
363+
try:
364+
data = json.loads(message)
365+
except json.JSONDecodeError:
366+
print(f"[Violetear] ⚠️ Received invalid JSON from client {client_id}")
367+
continue
368+
369+
# Dispatch 'realtime' function calls
370+
if data.get("type") == "realtime":
371+
func_name = data.get("func")
372+
args = data.get("args", [])
373+
kwargs = data.get("kwargs", {})
374+
375+
if func_name in self.server.realtime_functions:
376+
func = self.server.realtime_functions[func_name]
377+
378+
# Execute the function (Fire-and-forget from client perspective)
379+
# We await it here so the server processes it safely within this connection's loop
380+
try:
381+
await func(*args, **kwargs)
382+
except Exception as e:
383+
print(f"[Violetear] ❌ Error executing realtime function '{func_name}': {e}")
384+
else:
385+
print(f"[Violetear] ⚠️ Warning: Client {client_id} tried to call unknown realtime function '{func_name}'")
386+
363387
except (WebSocketDisconnect, RuntimeError):
364388
await self.socket_manager.disconnect(client_id)
365389

violetear/markup.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,82 @@ def _write_line(self, fp, value, indent=0):
6161
fp.write(value)
6262

6363

64+
class ElementBuilder:
65+
"""
66+
Helper class returned by the 'with element as e:' context manager.
67+
It allows creating siblings attached to the parent element using a fluent syntax.
68+
"""
69+
def __init__(self, parent: Element):
70+
self._parent = parent
71+
72+
# Register common HTML tags as methods on this instance
73+
# This allows: e.div(...), e.h1(...)
74+
self.div = self._factory("div")
75+
self.section = self._factory("section")
76+
self.header = self._factory("header")
77+
self.footer = self._factory("footer")
78+
self.main = self._factory("main")
79+
self.nav = self._factory("nav")
80+
81+
self.h1 = self._factory("h1")
82+
self.h2 = self._factory("h2")
83+
self.h3 = self._factory("h3")
84+
self.h4 = self._factory("h4")
85+
self.h5 = self._factory("h5")
86+
self.h6 = self._factory("h6")
87+
88+
self.p = self._factory("p")
89+
self.span = self._factory("span")
90+
self.a = self._factory("a")
91+
self.img = self._factory("img")
92+
93+
self.ul = self._factory("ul")
94+
self.ol = self._factory("ol")
95+
self.li = self._factory("li")
96+
97+
self.table = self._factory("table")
98+
self.thead = self._factory("thead")
99+
self.tbody = self._factory("tbody")
100+
self.tr = self._factory("tr")
101+
self.td = self._factory("td")
102+
self.th = self._factory("th")
103+
104+
self.form = self._factory("form")
105+
self.input = self._factory("input")
106+
self.button = self._factory("button")
107+
self.label = self._factory("label")
108+
self.select = self._factory("select")
109+
self.option = self._factory("option")
110+
self.textarea = self._factory("textarea")
111+
112+
def tag(self, name: str, *content, **kwargs) -> Element:
113+
"""
114+
Explicitly create a tag that isn't pre-defined.
115+
Usage: e.tag("my-custom-component", ...)
116+
"""
117+
el = Element(name, *content, **kwargs)
118+
self._parent.add(el)
119+
return el
120+
121+
def _factory(self, tag_name: str):
122+
"""
123+
Internal helper that returns a function to create a specific tag.
124+
"""
125+
def wrapper(*content, **kwargs) -> Element:
126+
return self.tag(tag_name, *content, **kwargs)
127+
128+
# improved debugging representation
129+
wrapper.__name__ = tag_name
130+
wrapper.__qualname__ = f"ElementBuilder.{tag_name}"
131+
return wrapper
132+
133+
64134
class Element(Markup):
65135
def __init__(
66136
self,
67137
tag: str,
68-
*content: Element,
69138
text: str | None = None,
139+
*content: Element,
70140
classes: str | List[str] | None = None,
71141
id: str | None = None,
72142
style: Style | None = None,
@@ -89,6 +159,20 @@ def __init__(
89159
self._parent = parent
90160
self._attrs = attrs
91161

162+
def __enter__(self) -> ElementBuilder:
163+
"""
164+
Enables the syntax:
165+
with doc.body as e:
166+
e.h1("Title")
167+
"""
168+
return ElementBuilder(self)
169+
170+
def __exit__(self, exc_type, exc_val, exc_tb):
171+
"""
172+
Standard context manager exit.
173+
"""
174+
return False # Propagate exceptions if they occur
175+
92176
def id(self, id: str) -> Self:
93177
self._id = id
94178
return self

0 commit comments

Comments
 (0)