77import json
88from pathlib import Path
99from textwrap import dedent
10- from typing import Any , Callable , Dict , List , Set , Union
10+ from typing import Any , Callable , Dict , List , Set , Union , cast
1111import uuid
1212
1313from .stylesheet import StyleSheet
14- from .markup import Document , StyleResource
14+ from .markup import Document
1515from .pwa import Manifest , ServiceWorker
16+ from .state import ReactiveProxy , local
1617
1718
1819# --- Optional Server Dependencies ---
@@ -161,10 +162,15 @@ class ClientRegistry:
161162 def __init__ (self , app : "App" ):
162163 self .app = app
163164 self .code_functions : Dict [str , Callable ] = {}
165+ self .state_classes : Dict [str , type ] = {}
164166 self .callback_names : Set [str ] = set ()
165167 self .realtime_functions : Dict [str , Callable ] = {}
166168 self .event_handlers : Dict [str , List [str ]] = {}
167169
170+ def register_state (self , cls : type ):
171+ """Registers a class to be transpiled to the client."""
172+ self .state_classes [cls .__name__ ] = cls
173+
168174 def _register (self , func : Callable ):
169175 """Helper to register the raw function source."""
170176 # Unwrap if it's already a stub (in case of decorator stacking)
@@ -397,6 +403,16 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str):
397403 self .client = ClientRegistry (self )
398404 self .server = ServerRegistry (self )
399405
406+ # UPDATED: self.local wrapper
407+ # This intercepts the decorator to register the class before passing it
408+ # to the logic in state.py
409+ def local [T ](self , cls : type [T ]) -> T :
410+ # 1. Register source code for the bundle
411+ self .client .register_state (cls )
412+
413+ # 2. Return the server-side proxy
414+ return local (cls )
415+
400416 def _register_rpc_route (self , func : Callable ):
401417 """
402418 Decorator to expose a function as a server-side RPC endpoint.
@@ -514,29 +530,57 @@ def _generate_bundle(self) -> str:
514530 # 1. Mock the 'app' object
515531 header = "class Event: pass"
516532
517- # 2. Inject violetear.dom module
518- # This allows 'from violetear.dom import Document' to work in the browser
533+ # 2. Inject violetear.state module
534+ state_path = Path (__file__ ).parent / "state.py"
535+ with open (state_path , "r" ) as f :
536+ state_source = f .read ()
537+
538+ state_injection = dedent (
539+ f"""
540+ import sys, types
541+
542+ # 1. Create or Get 'violetear' parent package
543+ if "violetear" not in sys.modules:
544+ m_violetear = types.ModuleType("violetear")
545+ sys.modules["violetear"] = m_violetear
546+ else:
547+ m_violetear = sys.modules["violetear"]
548+
549+ # 2. Create 'violetear.state'
550+ m_state = types.ModuleType("violetear.state")
551+ sys.modules["violetear.state"] = m_state
552+
553+ # 3. Link child to parent (Critical for imports to work)
554+ m_violetear.state = m_state
555+
556+ # 4. Execute source
557+ exec({ repr (state_source )} , m_state.__dict__)
558+
559+ # 5. Global Import (Makes 'violetear' available in this script)
560+ import violetear.state
561+ """
562+ )
563+
564+ # 3. Inject violetear.dom module
519565 dom_path = Path (__file__ ).parent / "dom.py"
520566 with open (dom_path , "r" ) as f :
521567 dom_source = f .read ()
522568
523569 dom_injection = dedent (
524570 f"""
525- import sys, types
526- # Create virtual module 'violetear'
527- m_violetear = types.ModuleType("violetear")
528- sys.modules["violetear"] = m_violetear
529-
530- # Create virtual module 'violetear.dom'
531571 m_dom = types.ModuleType("violetear.dom")
532572 sys.modules["violetear.dom"] = m_dom
533573
534- # Execute source
574+ # Link to parent
575+ sys.modules["violetear"].dom = m_dom
576+
535577 exec({ repr (dom_source )} , m_dom.__dict__)
578+
579+ import violetear.dom
536580 """
537581 )
538582
539- # Inject Storage
583+ # 4. Inject Storage
540584 storage_path = Path (__file__ ).parent / "storage.py"
541585 with open (storage_path , "r" ) as f :
542586 storage_source = f .read ()
@@ -545,61 +589,89 @@ def _generate_bundle(self) -> str:
545589 f"""
546590 m_storage = types.ModuleType("violetear.storage")
547591 sys.modules["violetear.storage"] = m_storage
592+
593+ # Link to parent
594+ sys.modules["violetear"].storage = m_storage
595+
548596 exec({ repr (storage_source )} , m_storage.__dict__)
597+
598+ import violetear.storage
549599 """
550600 )
551601
552- # 3 . Read the Client Runtime (Hydration logic)
602+ # 5 . Read the Client Runtime
553603 runtime_path = Path (__file__ ).parent / "client.py"
554604 with open (runtime_path , "r" ) as f :
555605 runtime_code = f .read ()
556606
557- # 4. Extract User Functions
607+ # 6. Generate State Classes (with dataclass re-application)
608+ state_code = []
609+ for name , cls in self .client .state_classes .items ():
610+ source = inspect .getsource (cls )
611+ lines = source .split ("\n " )
612+
613+ # Check for dataclass
614+ is_dataclass = "@dataclass" in source
615+
616+ # Strip decorators to avoid 'app' definition errors
617+ clean_lines = [l for l in lines if not l .strip ().startswith ("@" )]
618+
619+ # Reconstruct class
620+ state_code .append ("\n " .join (clean_lines ))
621+
622+ # Re-apply dataclass
623+ if is_dataclass :
624+ state_code .append (f"{ name } = dataclass({ name } )" )
625+
626+ # Apply Reactive Proxy
627+ # Now safe because we imported violetear.state above
628+ state_code .append (f"{ name } = violetear.state.local({ name } )" )
629+
630+ # 7. Extract User Functions
558631 user_code = []
559632 for name , func in self .client .code_functions .items ():
560633 code = inspect .getsource (func ).split ("\n " )
561- code = [c for c in code if not c .startswith ("@" )] # remove decorators
634+ code = [c for c in code if not c .startswith ("@" )]
562635 user_code .append ("\n " .join (code ))
563636
564- # --- SAFETY INJECTION START ---
565- # We attach a dummy .broadcast() method to client functions running in the browser.
566- # This prevents confusion if a user tries to call await my_func.broadcast() in client code.
637+ # --- Safety Checks & Server Stubs ---
567638 safety_checks = []
568639 safety_checks .append (
569640 dedent (
570641 """
571642 def _server_only_broadcast(*args, **kwargs):
572- raise RuntimeError("❌ .broadcast() cannot be called from the Client (Browser). \\ nIt must be called from the Server to trigger client updates .")
643+ raise RuntimeError("❌ .broadcast() cannot be called from the Client.")
573644 def _server_only_invoke(*args, **kwargs):
574- raise RuntimeError("❌ .invoke() cannot be called from the Client (Browser). \\ nIt must be called from the Server to trigger client updates .")
645+ raise RuntimeError("❌ .invoke() cannot be called from the Client.")
575646 """
576647 )
577648 )
578-
579649 for name in self .client .realtime_functions .keys ():
580650 safety_checks .append (f"{ name } .broadcast = _server_only_broadcast" )
581651 safety_checks .append (f"{ name } .invoke = _server_only_invoke" )
582652
583653 safety_code = "\n " .join (safety_checks )
584- # --- SAFETY INJECTION END ---
585-
586- # 5. Generate Server Stubs
587654 server_stubs = self ._generate_server_stubs ()
588655
589- # 6 . Initialization
656+ # 8 . Initialization
590657 init_code = "# --- Init ---\n hydrate(globals())"
591-
592- # Run startup functions
593658 for func_name in self .client .event_handlers .get ("ready" , []):
594659 init_code += f"\n await { func_name } ()"
595660
661+ # Global Imports for the Bundle
662+ # Ensure standard library + violetear components are ready
663+ imports = "from dataclasses import dataclass, field\n import datetime\n import json"
664+
596665 return "\n \n " .join (
597666 [
667+ imports ,
598668 header ,
669+ state_injection ,
599670 dom_injection ,
600671 storage_injection ,
601672 runtime_code ,
602- "\n \n " .join (user_code ),
673+ "\n " .join (state_code ),
674+ "\n " .join (user_code ),
603675 safety_code ,
604676 server_stubs ,
605677 init_code ,
0 commit comments