pulse-framework 0.1.65__tar.gz → 0.1.66a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/PKG-INFO +1 -1
  2. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/pyproject.toml +1 -1
  3. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/app.py +66 -16
  4. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/templates/layout.py +3 -8
  5. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/render_session.py +62 -0
  6. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/README.md +0 -0
  7. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/__init__.py +0 -0
  8. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/_examples.py +0 -0
  9. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/channel.py +0 -0
  10. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/__init__.py +0 -0
  11. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/cmd.py +0 -0
  12. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/dependencies.py +0 -0
  13. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/folder_lock.py +0 -0
  14. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/helpers.py +0 -0
  15. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/logging.py +0 -0
  16. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/models.py +0 -0
  17. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/packages.py +0 -0
  18. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/processes.py +0 -0
  19. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/secrets.py +0 -0
  20. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cli/uvicorn_log_config.py +0 -0
  21. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/code_analysis.py +0 -0
  22. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/__init__.py +0 -0
  23. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/codegen.py +0 -0
  24. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/templates/__init__.py +0 -0
  25. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/templates/route.py +0 -0
  26. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/templates/routes_ts.py +0 -0
  27. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/codegen/utils.py +0 -0
  28. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/component.py +0 -0
  29. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/components/__init__.py +0 -0
  30. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/components/for_.py +0 -0
  31. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/components/if_.py +0 -0
  32. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/components/react_router.py +0 -0
  33. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/context.py +0 -0
  34. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/cookies.py +0 -0
  35. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/decorators.py +0 -0
  36. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/__init__.py +0 -0
  37. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/elements.py +0 -0
  38. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/events.py +0 -0
  39. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/props.py +0 -0
  40. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/svg.py +0 -0
  41. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/tags.py +0 -0
  42. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/dom/tags.pyi +0 -0
  43. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/env.py +0 -0
  44. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/forms.py +0 -0
  45. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/helpers.py +0 -0
  46. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/__init__.py +0 -0
  47. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/core.py +0 -0
  48. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/effects.py +0 -0
  49. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/init.py +0 -0
  50. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/runtime.py +0 -0
  51. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/setup.py +0 -0
  52. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/stable.py +0 -0
  53. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/hooks/state.py +0 -0
  54. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/__init__.py +0 -0
  55. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/__init__.pyi +0 -0
  56. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/_types.py +0 -0
  57. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/array.py +0 -0
  58. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/console.py +0 -0
  59. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/date.py +0 -0
  60. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/document.py +0 -0
  61. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/error.py +0 -0
  62. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/json.py +0 -0
  63. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/map.py +0 -0
  64. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/math.py +0 -0
  65. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/navigator.py +0 -0
  66. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/number.py +0 -0
  67. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/obj.py +0 -0
  68. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/object.py +0 -0
  69. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/promise.py +0 -0
  70. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/pulse.py +0 -0
  71. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/react.py +0 -0
  72. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/regexp.py +0 -0
  73. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/set.py +0 -0
  74. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/string.py +0 -0
  75. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/weakmap.py +0 -0
  76. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/weakset.py +0 -0
  77. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/js/window.py +0 -0
  78. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/messages.py +0 -0
  79. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/middleware.py +0 -0
  80. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/plugin.py +0 -0
  81. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/proxy.py +0 -0
  82. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/py.typed +0 -0
  83. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/__init__.py +0 -0
  84. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/client.py +0 -0
  85. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/common.py +0 -0
  86. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/effect.py +0 -0
  87. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/infinite_query.py +0 -0
  88. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/mutation.py +0 -0
  89. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/protocol.py +0 -0
  90. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/query.py +0 -0
  91. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/queries/store.py +0 -0
  92. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/react_component.py +0 -0
  93. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/reactive.py +0 -0
  94. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/reactive_extensions.py +0 -0
  95. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/renderer.py +0 -0
  96. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/request.py +0 -0
  97. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/requirements.py +0 -0
  98. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/routing.py +0 -0
  99. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/scheduling.py +0 -0
  100. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/serializer.py +0 -0
  101. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/state.py +0 -0
  102. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/test_helpers.py +0 -0
  103. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/__init__.py +0 -0
  104. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/assets.py +0 -0
  105. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/builtins.py +0 -0
  106. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/dynamic_import.py +0 -0
  107. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/emit_context.py +0 -0
  108. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/errors.py +0 -0
  109. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/function.py +0 -0
  110. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/id.py +0 -0
  111. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/imports.py +0 -0
  112. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/js_module.py +0 -0
  113. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/__init__.py +0 -0
  114. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/asyncio.py +0 -0
  115. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/json.py +0 -0
  116. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/math.py +0 -0
  117. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  118. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
  119. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/modules/typing.py +0 -0
  120. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/nodes.py +0 -0
  121. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/py_module.py +0 -0
  122. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/transpiler.py +0 -0
  123. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/transpiler/vdom.py +0 -0
  124. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/types/__init__.py +0 -0
  125. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/types/event_handler.py +0 -0
  126. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/user_session.py +0 -0
  127. {pulse_framework-0.1.65 → pulse_framework-0.1.66a2}/src/pulse/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.65
3
+ Version: 0.1.66a2
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
6
  Requires-Dist: fastapi>=0.128.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.65"
3
+ version = "0.1.66a2"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -107,8 +107,8 @@ PulseMode = Literal["subdomains", "single-server"]
107
107
  """Deployment mode for the application.
108
108
 
109
109
  Values:
110
- "single-server": Python and React served from the same origin (default).
111
- "subdomains": Python API on a subdomain (e.g., api.example.com).
110
+ "single-server": Python and React served from the same origin (default).
111
+ "subdomains": Python API on a subdomain (e.g., api.example.com).
112
112
  """
113
113
 
114
114
 
@@ -118,12 +118,12 @@ class ConnectionStatusConfig:
118
118
  Configuration for connection status message delays.
119
119
 
120
120
  Attributes:
121
- initial_connecting_delay: Delay in seconds before showing "Connecting..." message
122
- on initial connection attempt. Default: 2.0
123
- initial_error_delay: Additional delay in seconds before showing error message
124
- on initial connection attempt (after connecting message). Default: 8.0
125
- reconnect_error_delay: Delay in seconds before showing error message when
126
- reconnecting after losing connection. Default: 8.0
121
+ initial_connecting_delay: Delay in seconds before showing "Connecting..." message
122
+ on initial connection attempt. Default: 2.0
123
+ initial_error_delay: Additional delay in seconds before showing error message
124
+ on initial connection attempt (after connecting message). Default: 8.0
125
+ reconnect_error_delay: Delay in seconds before showing error message when
126
+ reconnecting after losing connection. Default: 8.0
127
127
  """
128
128
 
129
129
  initial_connecting_delay: float = 2.0
@@ -171,15 +171,15 @@ class App:
171
171
  import pulse as ps
172
172
 
173
173
  app = ps.App(
174
- routes=[
175
- ps.Route("/", render=home),
176
- ps.Route("/users/:id", render=user_detail),
177
- ],
178
- session_timeout=120.0,
174
+ routes=[
175
+ ps.Route("/", render=home),
176
+ ps.Route("/users/:id", render=user_detail),
177
+ ],
178
+ session_timeout=120.0,
179
179
  )
180
180
 
181
181
  if __name__ == "__main__":
182
- app.run(port=8000)
182
+ app.run(port=8000)
183
183
  ```
184
184
  """
185
185
 
@@ -544,10 +544,23 @@ class App:
544
544
  paths = [ensure_absolute_path(path) for path in paths]
545
545
  payload["paths"] = paths
546
546
  route_info = payload.get("routeInfo")
547
+ debug = os.environ.get("PULSE_DEBUG_RENDER")
548
+ if debug:
549
+ print(
550
+ "[PulseDebug][prerender] session=%s header_render_id=%s payload_render_id=%s paths=%s route_info=%s"
551
+ % (
552
+ session.sid,
553
+ request.headers.get("x-pulse-render-id"),
554
+ payload.get("renderId"),
555
+ paths,
556
+ route_info,
557
+ )
558
+ )
547
559
 
548
560
  client_addr: str | None = get_client_address(request)
549
561
  # Reuse render session from header (set by middleware) or create new one
550
562
  render = PulseContext.get().render
563
+ reused = render is not None
551
564
  if render is not None:
552
565
  render_id = render.id
553
566
  else:
@@ -556,9 +569,15 @@ class App:
556
569
  render = self.create_render(
557
570
  render_id, session, client_address=client_addr
558
571
  )
572
+ if debug:
573
+ print(
574
+ "[PulseDebug][prerender] session=%s render=%s reused=%s connected=%s"
575
+ % (session.sid, render_id, reused, render.connected)
576
+ )
559
577
 
560
578
  # Schedule cleanup timeout (will cancel/reschedule on activity)
561
- self._schedule_render_cleanup(render_id)
579
+ if not render.connected:
580
+ self._schedule_render_cleanup(render_id)
562
581
 
563
582
  def _normalize_prerender_result(
564
583
  captured: ServerInitMessage | ServerNavigateToMessage,
@@ -692,12 +711,16 @@ class App:
692
711
  ):
693
712
  # Expect renderId during websocket auth and require a valid user session
694
713
  rid = auth.get("render_id") if auth else None
695
-
696
714
  # Parse cookies from environ and ensure a session exists
697
715
  cookie = self.cookie.get_from_socketio(environ)
698
716
  if cookie is None:
699
717
  raise ConnectionRefusedError("Socket connect missing cookie")
700
718
  session = await self.get_or_create_session(cookie)
719
+ debug = os.environ.get("PULSE_DEBUG_RENDER")
720
+ if debug:
721
+ print(
722
+ "[PulseDebug][connect] session=%s render_id=%s" % (session.sid, rid)
723
+ )
701
724
 
702
725
  if not rid:
703
726
  # Still refuse connections without a renderId
@@ -708,6 +731,12 @@ class App:
708
731
  # Allow reconnects where the provided renderId no longer exists by creating a new RenderSession
709
732
  render = self.render_sessions.get(rid)
710
733
  if render is None:
734
+ # The client will try to attach to a non-existing RouteMount, which will cause a reload down the line
735
+ if debug:
736
+ print(
737
+ "[PulseDebug][connect] render_missing session=%s render_id=%s creating=true"
738
+ % (session.sid, rid)
739
+ )
711
740
  render = self.create_render(
712
741
  rid, session, client_address=get_client_address_socketio(environ)
713
742
  )
@@ -718,6 +747,11 @@ class App:
718
747
  f"Socket connect session mismatch render={render.id} "
719
748
  + f"owner={owner} session={session.sid}"
720
749
  )
750
+ if debug:
751
+ print(
752
+ "[PulseDebug][connect] render_found session=%s render_id=%s owner=%s connected=%s"
753
+ % (session.sid, render.id, owner, render.connected)
754
+ )
721
755
 
722
756
  def on_message(message: ServerMessage):
723
757
  payload = serialize(message)
@@ -830,6 +864,22 @@ class App:
830
864
  async def _handle_pulse_message(
831
865
  self, render: RenderSession, session: UserSession, msg: ClientPulseMessage
832
866
  ) -> None:
867
+ if os.environ.get("PULSE_DEBUG_RENDER") and msg["type"] in (
868
+ "attach",
869
+ "update",
870
+ "detach",
871
+ ):
872
+ print(
873
+ "[PulseDebug][client-message] session=%s render=%s type=%s path=%s route_info=%s"
874
+ % (
875
+ session.sid,
876
+ render.id,
877
+ msg["type"],
878
+ msg.get("path"),
879
+ msg.get("routeInfo"),
880
+ )
881
+ )
882
+
833
883
  async def _next() -> Ok[None]:
834
884
  if msg["type"] == "attach":
835
885
  render.attach(msg["path"], msg["routeInfo"])
@@ -53,15 +53,11 @@ export async function loader(args: LoaderFunctionArgs) {
53
53
  return data(prerenderData, { headers });
54
54
  }
55
55
 
56
- // Client loader: re-prerender on navigation while reusing renderId
56
+ // Client loader: re-prerender on navigation while reusing directives
57
57
  export async function clientLoader(args: ClientLoaderFunctionArgs) {
58
58
  const url = new URL(args.request.url);
59
59
  const matches = matchRoutes(rrPulseRouteTree, url.pathname) ?? [];
60
60
  const paths = matches.map(m => m.route.uniquePath);
61
- const renderId =
62
- typeof window !== "undefined" && typeof sessionStorage !== "undefined"
63
- ? (sessionStorage.getItem("__PULSE_RENDER_ID") ?? undefined)
64
- : undefined;
65
61
  const directives =
66
62
  typeof window !== "undefined" && typeof sessionStorage !== "undefined"
67
63
  ? (JSON.parse(sessionStorage.getItem("__PULSE_DIRECTIVES") ?? "{}"))
@@ -76,7 +72,7 @@ export async function clientLoader(args: ClientLoaderFunctionArgs) {
76
72
  method: "POST",
77
73
  headers,
78
74
  credentials: "include",
79
- body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args), renderId }),
75
+ body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args) }),
80
76
  });
81
77
  if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
82
78
  const body = await res.json();
@@ -92,7 +88,6 @@ export async function clientLoader(args: ClientLoaderFunctionArgs) {
92
88
  export default function PulseLayout() {
93
89
  const data = useLoaderData<typeof loader>();
94
90
  if (typeof window !== "undefined" && typeof sessionStorage !== "undefined") {
95
- sessionStorage.setItem("__PULSE_RENDER_ID", data.renderId);
96
91
  sessionStorage.setItem("__PULSE_DIRECTIVES", JSON.stringify(data.directives));
97
92
  }
98
93
  return (
@@ -101,6 +96,6 @@ export default function PulseLayout() {
101
96
  </PulseProvider>
102
97
  );
103
98
  }
104
- // Persist renderId and directives in sessionStorage for reuse in clientLoader is handled within the component
99
+ // Persist directives in sessionStorage for reuse in clientLoader is handled within the component
105
100
  """
106
101
  )
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
+ import os
3
4
  import traceback
4
5
  import uuid
5
6
  from asyncio import iscoroutine
@@ -137,6 +138,11 @@ class RouteMount:
137
138
  if self.state != "pending":
138
139
  return
139
140
  action = self.pending_action
141
+ if os.environ.get("PULSE_DEBUG_RENDER"):
142
+ print(
143
+ "[PulseDebug][mount-timeout] render=%s path=%s action=%s state=%s"
144
+ % (self.render.id, self.path, action, self.state)
145
+ )
140
146
  self.pending_action = None
141
147
  if action == "dispose":
142
148
  self.render.dispose_mount(self.path, self)
@@ -144,6 +150,11 @@ class RouteMount:
144
150
  self.to_idle()
145
151
 
146
152
  def start_pending(self, timeout: float, *, action: PendingAction = "idle") -> None:
153
+ if os.environ.get("PULSE_DEBUG_RENDER"):
154
+ print(
155
+ "[PulseDebug][mount-pending] render=%s path=%s state=%s action=%s timeout=%s"
156
+ % (self.render.id, self.path, self.state, action, timeout)
157
+ )
147
158
  if self.state == "pending":
148
159
  prev_action = self.pending_action
149
160
  next_action: PendingAction = (
@@ -166,6 +177,16 @@ class RouteMount:
166
177
  )
167
178
 
168
179
  def activate(self, send_message: Callable[[ServerMessage], Any]) -> None:
180
+ if os.environ.get("PULSE_DEBUG_RENDER"):
181
+ print(
182
+ "[PulseDebug][mount-activate] render=%s path=%s state=%s queued=%s"
183
+ % (
184
+ self.render.id,
185
+ self.path,
186
+ self.state,
187
+ 0 if not self.queue else len(self.queue),
188
+ )
189
+ )
169
190
  if self.state != "pending":
170
191
  return
171
192
  self._cancel_pending_timeout()
@@ -192,6 +213,11 @@ class RouteMount:
192
213
  def to_idle(self) -> None:
193
214
  if self.state != "pending":
194
215
  return
216
+ if os.environ.get("PULSE_DEBUG_RENDER"):
217
+ print(
218
+ "[PulseDebug][mount-idle] render=%s path=%s"
219
+ % (self.render.id, self.path)
220
+ )
195
221
  self.state = "idle"
196
222
  self.queue = None
197
223
  self._cancel_pending_timeout()
@@ -317,11 +343,15 @@ class RenderSession:
317
343
 
318
344
  def connect(self, send_message: Callable[[ServerMessage], Any]):
319
345
  """WebSocket connected. Set sender, don't auto-flush (attach does that)."""
346
+ if os.environ.get("PULSE_DEBUG_RENDER"):
347
+ print("[PulseDebug][render-connect] render=%s" % self.id)
320
348
  self._send_message = send_message
321
349
  self.connected = True
322
350
 
323
351
  def disconnect(self):
324
352
  """WebSocket disconnected. Start queuing briefly before pausing."""
353
+ if os.environ.get("PULSE_DEBUG_RENDER"):
354
+ print("[PulseDebug][render-disconnect] render=%s" % self.id)
325
355
  self._send_message = None
326
356
  self.connected = False
327
357
 
@@ -395,6 +425,11 @@ class RenderSession:
395
425
  - Creates mounts in PENDING state and starts queue
396
426
  """
397
427
  normalized = [ensure_absolute_path(path) for path in paths]
428
+ if os.environ.get("PULSE_DEBUG_RENDER"):
429
+ print(
430
+ "[PulseDebug][prerender] render=%s paths=%s route_info=%s"
431
+ % (self.id, normalized, route_info)
432
+ )
398
433
 
399
434
  results: dict[str, ServerInitMessage | ServerNavigateToMessage] = {}
400
435
 
@@ -402,6 +437,12 @@ class RenderSession:
402
437
  route = self.routes.find(path)
403
438
  info = route_info or route.default_route_info()
404
439
  mount = self.route_mounts.get(path)
440
+ if os.environ.get("PULSE_DEBUG_RENDER"):
441
+ route_label = repr(route)
442
+ print(
443
+ "[PulseDebug][prerender] render=%s path=%s mount_state=%s route=%s"
444
+ % (self.id, path, mount.state if mount else None, route_label)
445
+ )
405
446
 
406
447
  if mount is None:
407
448
  mount = RouteMount(self, path, route, info)
@@ -440,6 +481,17 @@ class RenderSession:
440
481
  mount = self.route_mounts.get(path)
441
482
 
442
483
  if mount is None or mount.state == "idle":
484
+ if os.environ.get("PULSE_DEBUG_RENDER"):
485
+ print(
486
+ "[PulseDebug][attach] render=%s path=%s mount_state=%s mounts=%s route_info=%s"
487
+ % (
488
+ self.id,
489
+ path,
490
+ mount.state if mount else None,
491
+ {key: value.state for key, value in self.route_mounts.items()},
492
+ route_info,
493
+ )
494
+ )
443
495
  # Initial render must come from prerender
444
496
  print(f"[DEBUG] Missing or idle route '{path}', reloading")
445
497
  self.send({"type": "reload"})
@@ -448,6 +500,11 @@ class RenderSession:
448
500
  # Update route info for active and pending mounts
449
501
  mount.update_route(route_info)
450
502
  if mount.state == "pending" and self._send_message:
503
+ if os.environ.get("PULSE_DEBUG_RENDER"):
504
+ print(
505
+ "[PulseDebug][attach] render=%s path=%s activating=true"
506
+ % (self.id, path)
507
+ )
451
508
  mount.activate(self._send_message)
452
509
 
453
510
  def update_route(self, path: str, route_info: RouteInfo):
@@ -463,6 +520,11 @@ class RenderSession:
463
520
  current = self.route_mounts.get(path)
464
521
  if current is not mount:
465
522
  return
523
+ if os.environ.get("PULSE_DEBUG_RENDER"):
524
+ print(
525
+ "[PulseDebug][mount-dispose] render=%s path=%s state=%s"
526
+ % (self.id, path, mount.state)
527
+ )
466
528
  try:
467
529
  self.route_mounts.pop(path, None)
468
530
  mount.dispose()