pulse-framework 0.1.71__tar.gz → 0.1.72__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 (128) hide show
  1. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/PKG-INFO +3 -3
  2. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/pyproject.toml +3 -3
  3. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/__init__.py +3 -0
  4. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/app.py +27 -24
  5. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/cmd.py +1 -1
  6. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/folder_lock.py +25 -6
  7. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/templates/layout.py +3 -1
  8. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/effects.py +15 -1
  9. pulse_framework-0.1.72/src/pulse/proxy.py +783 -0
  10. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/common.py +16 -4
  11. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/infinite_query.py +12 -2
  12. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/query.py +2 -1
  13. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/render_session.py +5 -2
  14. pulse_framework-0.1.71/src/pulse/proxy.py +0 -249
  15. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/README.md +0 -0
  16. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/_examples.py +0 -0
  17. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/channel.py +0 -0
  18. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/__init__.py +0 -0
  19. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/dependencies.py +0 -0
  20. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/helpers.py +0 -0
  21. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/logging.py +0 -0
  22. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/models.py +0 -0
  23. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/packages.py +0 -0
  24. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/processes.py +0 -0
  25. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/secrets.py +0 -0
  26. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cli/uvicorn_log_config.py +0 -0
  27. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/code_analysis.py +0 -0
  28. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/__init__.py +0 -0
  29. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/codegen.py +0 -0
  30. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/templates/__init__.py +0 -0
  31. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/templates/route.py +0 -0
  32. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/templates/routes_ts.py +0 -0
  33. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/codegen/utils.py +0 -0
  34. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/component.py +0 -0
  35. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/components/__init__.py +0 -0
  36. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/components/for_.py +0 -0
  37. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/components/if_.py +0 -0
  38. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/components/react_router.py +0 -0
  39. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/context.py +0 -0
  40. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/cookies.py +0 -0
  41. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/decorators.py +0 -0
  42. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/__init__.py +0 -0
  43. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/elements.py +0 -0
  44. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/events.py +0 -0
  45. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/props.py +0 -0
  46. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/svg.py +0 -0
  47. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/tags.py +0 -0
  48. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/dom/tags.pyi +0 -0
  49. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/env.py +0 -0
  50. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/forms.py +0 -0
  51. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/helpers.py +0 -0
  52. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/__init__.py +0 -0
  53. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/core.py +0 -0
  54. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/init.py +0 -0
  55. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/runtime.py +0 -0
  56. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/setup.py +0 -0
  57. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/stable.py +0 -0
  58. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/hooks/state.py +0 -0
  59. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/__init__.py +0 -0
  60. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/__init__.pyi +0 -0
  61. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/_types.py +0 -0
  62. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/array.py +0 -0
  63. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/console.py +0 -0
  64. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/date.py +0 -0
  65. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/document.py +0 -0
  66. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/error.py +0 -0
  67. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/json.py +0 -0
  68. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/map.py +0 -0
  69. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/math.py +0 -0
  70. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/navigator.py +0 -0
  71. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/number.py +0 -0
  72. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/obj.py +0 -0
  73. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/object.py +0 -0
  74. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/promise.py +0 -0
  75. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/pulse.py +0 -0
  76. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/react.py +0 -0
  77. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/regexp.py +0 -0
  78. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/set.py +0 -0
  79. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/string.py +0 -0
  80. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/weakmap.py +0 -0
  81. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/weakset.py +0 -0
  82. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/js/window.py +0 -0
  83. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/messages.py +0 -0
  84. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/middleware.py +0 -0
  85. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/plugin.py +0 -0
  86. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/py.typed +0 -0
  87. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/__init__.py +0 -0
  88. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/client.py +0 -0
  89. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/effect.py +0 -0
  90. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/mutation.py +0 -0
  91. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/protocol.py +0 -0
  92. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/queries/store.py +0 -0
  93. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/react_component.py +0 -0
  94. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/reactive.py +0 -0
  95. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/reactive_extensions.py +0 -0
  96. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/renderer.py +0 -0
  97. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/request.py +0 -0
  98. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/requirements.py +0 -0
  99. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/routing.py +0 -0
  100. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/scheduling.py +0 -0
  101. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/serializer.py +0 -0
  102. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/state.py +0 -0
  103. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/test_helpers.py +0 -0
  104. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/__init__.py +0 -0
  105. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/assets.py +0 -0
  106. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/builtins.py +0 -0
  107. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/dynamic_import.py +0 -0
  108. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/emit_context.py +0 -0
  109. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/errors.py +0 -0
  110. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/function.py +0 -0
  111. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/id.py +0 -0
  112. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/imports.py +0 -0
  113. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/js_module.py +0 -0
  114. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/__init__.py +0 -0
  115. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/asyncio.py +0 -0
  116. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/json.py +0 -0
  117. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/math.py +0 -0
  118. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  119. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
  120. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/typing.py +0 -0
  121. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/nodes.py +0 -0
  122. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/py_module.py +0 -0
  123. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/transpiler.py +0 -0
  124. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/transpiler/vdom.py +0 -0
  125. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/types/__init__.py +0 -0
  126. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/types/event_handler.py +0 -0
  127. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/user_session.py +0 -0
  128. {pulse_framework-0.1.71 → pulse_framework-0.1.72}/src/pulse/version.py +0 -0
@@ -1,20 +1,20 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.71
3
+ Version: 0.1.72
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
- Requires-Dist: websockets>=12.0
6
5
  Requires-Dist: fastapi>=0.128.0
7
6
  Requires-Dist: uvicorn>=0.24.0
8
7
  Requires-Dist: mako>=1.3.10
9
8
  Requires-Dist: typer>=0.16.0
10
9
  Requires-Dist: python-socketio>=5.16.0
11
10
  Requires-Dist: rich>=13.7.1
12
- Requires-Dist: python-multipart>=0.0.20
11
+ Requires-Dist: python-multipart>=0.0.22
13
12
  Requires-Dist: python-dateutil>=2.9.0.post0
14
13
  Requires-Dist: starlette>=0.50.0,<0.51.0
15
14
  Requires-Dist: urllib3>=2.6.3
16
15
  Requires-Dist: watchfiles>=1.1.0
17
16
  Requires-Dist: httpx>=0.28.1
17
+ Requires-Dist: aiohttp>=3.12.0
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
 
@@ -1,23 +1,23 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.71"
3
+ version = "0.1.72"
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"
7
7
  dependencies = [
8
- "websockets>=12.0",
9
8
  "fastapi>=0.128.0",
10
9
  "uvicorn>=0.24.0",
11
10
  "mako>=1.3.10",
12
11
  "typer>=0.16.0",
13
12
  "python-socketio>=5.16.0",
14
13
  "rich>=13.7.1",
15
- "python-multipart>=0.0.20",
14
+ "python-multipart>=0.0.22",
16
15
  "python-dateutil>=2.9.0.post0",
17
16
  "starlette>=0.50.0,<0.51.0",
18
17
  "urllib3>=2.6.3",
19
18
  "watchfiles>=1.1.0",
20
19
  "httpx>=0.28.1",
20
+ "aiohttp>=3.12.0",
21
21
  ]
22
22
 
23
23
  [tool.uv]
@@ -1322,6 +1322,9 @@ from pulse.middleware import (
1322
1322
 
1323
1323
  # Plugin
1324
1324
  from pulse.plugin import Plugin as Plugin
1325
+
1326
+ # Proxy
1327
+ from pulse.proxy import ProxyConfig as ProxyConfig
1325
1328
  from pulse.queries.client import QueryClient as QueryClient
1326
1329
  from pulse.queries.client import QueryFilter as QueryFilter
1327
1330
  from pulse.queries.client import queries as queries
@@ -67,7 +67,7 @@ from pulse.middleware import (
67
67
  Redirect,
68
68
  )
69
69
  from pulse.plugin import Plugin
70
- from pulse.proxy import ReactProxy
70
+ from pulse.proxy import ProxyConfig, ReactProxy
71
71
  from pulse.render_session import RenderSession
72
72
  from pulse.request import PulseRequest
73
73
  from pulse.routing import Layout, Route, RouteTree, ensure_absolute_path
@@ -211,6 +211,7 @@ class App:
211
211
  _tasks: TaskRegistry
212
212
  _timers: TimerRegistry
213
213
  _proxy: ReactProxy | None
214
+ proxy_config: ProxyConfig | None
214
215
  session_timeout: float
215
216
  connection_status: ConnectionStatusConfig
216
217
  render_loop_limit: int
@@ -232,6 +233,7 @@ class App:
232
233
  not_found: str = "/not-found",
233
234
  # Deployment and integration options
234
235
  mode: PulseMode = "single-server",
236
+ proxy: ProxyConfig | None = None,
235
237
  api_prefix: str = "/_pulse",
236
238
  cors: CORSOptions | None = None,
237
239
  fastapi: dict[str, Any] | None = None,
@@ -245,6 +247,7 @@ class App:
245
247
  # Resolve mode from environment and expose on the app instance
246
248
  self.env = envvars.pulse_env
247
249
  self.mode = mode
250
+ self.proxy_config = proxy
248
251
  self.status = AppStatus.created
249
252
  # Persist the server address for use by sessions (API calls, etc.) in ci/prod.
250
253
  self.server_address = server_address if self.env in ("ci", "prod") else None
@@ -506,15 +509,22 @@ class App:
506
509
  )
507
510
  render_id = request.headers.get("x-pulse-render-id")
508
511
  render = self._get_render_for_session(render_id, session)
509
- with PulseContext.update(session=session, render=render):
510
- res: Response = await call_next(request)
511
- session.handle_response(res)
512
-
513
- self._sessions_in_request[session.sid] -= 1
514
- if self._sessions_in_request[session.sid] == 0:
515
- del self._sessions_in_request[session.sid]
516
-
517
- return res
512
+ try:
513
+ with PulseContext.update(session=session, render=render):
514
+ res: Response = await call_next(request)
515
+ session.handle_response(res)
516
+ return res
517
+ except RuntimeError as exc:
518
+ # Client disconnected before response was sent. This happens when
519
+ # ASGI handlers (like the proxy) return early on disconnect without
520
+ # sending a response, which is valid ASGI but breaks BaseHTTPMiddleware.
521
+ if "No response returned" in str(exc):
522
+ return Response(status_code=499)
523
+ raise
524
+ finally:
525
+ self._sessions_in_request[session.sid] -= 1
526
+ if self._sessions_in_request[session.sid] == 0:
527
+ del self._sessions_in_request[session.sid]
518
528
 
519
529
  # Apply prefix to all routes
520
530
  prefix = self.api_prefix
@@ -654,10 +664,8 @@ class App:
654
664
  for plugin in self.plugins:
655
665
  plugin.on_setup(self)
656
666
 
657
- # In single-server mode, add catch-all route to proxy unmatched requests to React server
658
- # This route must be registered last so FastAPI tries all specific routes first
659
- # FastAPI will match specific routes before this catch-all, but we add an explicit check
660
- # as a safety measure to ensure API routes are never proxied
667
+ # In single-server mode, add catch-all route to proxy unmatched requests to React server.
668
+ # This route must be registered last so FastAPI tries all specific routes first.
661
669
  if self.mode == "single-server":
662
670
  react_server_address = envvars.react_server_address
663
671
  if not react_server_address:
@@ -666,11 +674,12 @@ class App:
666
674
  + "Use 'pulse run' CLI command or set the environment variable."
667
675
  )
668
676
 
669
- self._proxy = ReactProxy(
677
+ proxy_handler = ReactProxy(
670
678
  react_server_address=react_server_address,
671
679
  server_address=server_address,
680
+ config=self.proxy_config,
672
681
  )
673
- proxy_handler = self._proxy
682
+ self._proxy = proxy_handler
674
683
 
675
684
  # In dev mode, proxy WebSocket connections to React Router (e.g. Vite HMR)
676
685
  # Socket.IO handles /socket.io/ at ASGI level before reaching FastAPI
@@ -680,14 +689,8 @@ class App:
680
689
  async def websocket_proxy(websocket: WebSocket, path: str): # pyright: ignore[reportUnusedFunction]
681
690
  await proxy_handler.proxy_websocket(websocket)
682
691
 
683
- @self.fastapi.api_route(
684
- "/{path:path}",
685
- methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
686
- include_in_schema=False,
687
- )
688
- async def proxy_catch_all(request: Request, path: str): # pyright: ignore[reportUnusedFunction]
689
- # Proxy all unmatched HTTP requests to React Router
690
- return await proxy_handler(request)
692
+ # Register ASGI-level catch-all last.
693
+ self.fastapi.mount("/", proxy_handler, name="react-proxy")
691
694
 
692
695
  @self.sio.event
693
696
  async def connect( # pyright: ignore[reportUnusedFunction]
@@ -233,7 +233,7 @@ def run(
233
233
  )
234
234
  commands.append(server_cmd)
235
235
 
236
- with FolderLock(web_root):
236
+ with FolderLock(web_root, address=address, port=port):
237
237
  try:
238
238
  exit_code = execute_commands(
239
239
  commands,
@@ -52,7 +52,7 @@ def _write_gitignore_for_lock(lock_path: Path) -> None:
52
52
  ensure_gitignore_has(lock_path.parent, lock_path.name)
53
53
 
54
54
 
55
- def _create_lock_file(lock_path: Path) -> None:
55
+ def _create_lock_file(lock_path: Path, *, address: str, port: int) -> None:
56
56
  """Create a lock file with current process information."""
57
57
  lock_path = Path(lock_path)
58
58
  _write_gitignore_for_lock(lock_path)
@@ -61,18 +61,26 @@ def _create_lock_file(lock_path: Path) -> None:
61
61
  info = _read_lock(lock_path) or {}
62
62
  pid = int(info.get("pid", 0) or 0)
63
63
  if pid and is_process_alive(pid):
64
+ existing_addr = info.get("address", address)
65
+ existing_port = info.get("port", port)
66
+ protocol = (
67
+ "http" if existing_addr in ("127.0.0.1", "localhost") else "https"
68
+ )
69
+ url = f"{protocol}://{existing_addr}:{existing_port}"
64
70
  raise RuntimeError(
65
- f"Another Pulse dev instance appears to be running (pid={pid}) for {lock_path.parent}."
71
+ f"Another Pulse dev instance is running at {url} (pid={pid})"
66
72
  )
67
73
  # Stale lock; continue to overwrite
68
74
 
69
- payload = {
75
+ payload: dict[str, Any] = {
70
76
  "pid": os.getpid(),
71
77
  "created_at": int(time.time()),
72
78
  "hostname": socket.gethostname(),
73
79
  "platform": platform.platform(),
74
80
  "python": platform.python_version(),
75
81
  "cwd": os.getcwd(),
82
+ "address": address,
83
+ "port": port,
76
84
  }
77
85
  try:
78
86
  lock_path.parent.mkdir(parents=True, exist_ok=True)
@@ -105,23 +113,34 @@ class FolderLock:
105
113
  and know not to delete the lock on exit.
106
114
 
107
115
  Example:
108
- with FolderLock(web_root):
116
+ with FolderLock(web_root, address="localhost", port=8000):
109
117
  # Protected region
110
118
  pass
111
119
  """
112
120
 
113
- def __init__(self, web_root: Path, *, filename: str = ".pulse/lock"):
121
+ def __init__(
122
+ self,
123
+ web_root: Path,
124
+ *,
125
+ address: str,
126
+ port: int,
127
+ filename: str = ".pulse/lock",
128
+ ):
114
129
  """
115
130
  Initialize FolderLock.
116
131
 
117
132
  Args:
118
133
  web_root: Path to the web root directory
134
+ address: Server address to store in lock file
135
+ port: Server port to store in lock file
119
136
  filename: Name of the lock file (default: ".pulse/lock")
120
137
  """
121
138
  self.lock_path: Path = lock_path_for_web_root(web_root, filename)
139
+ self.address: str = address
140
+ self.port: int = port
122
141
 
123
142
  def __enter__(self):
124
- _create_lock_file(self.lock_path)
143
+ _create_lock_file(self.lock_path, address=self.address, port=self.port)
125
144
  return self
126
145
 
127
146
  def __exit__(
@@ -32,7 +32,9 @@ export async function loader(args: LoaderFunctionArgs) {
32
32
  if (cookie) fwd.set("cookie", cookie);
33
33
  if (authorization) fwd.set("authorization", authorization);
34
34
  fwd.set("content-type", "application/json");
35
- const res = await fetch(`${internal_server_address}$${"{"}config.apiPrefix}/prerender`, {
35
+ // Internal server address for server-side loader requests.
36
+ const internalServerAddress = "${internal_server_address}";
37
+ const res = await fetch(`$${"{"}internalServerAddress}$${"{"}config.apiPrefix}/prerender`, {
36
38
  method: "POST",
37
39
  headers: fwd,
38
40
  body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args) }),
@@ -2,7 +2,7 @@ from collections.abc import Callable
2
2
  from typing import Any, override
3
3
 
4
4
  from pulse.hooks.core import HookMetadata, HookState, hooks
5
- from pulse.reactive import AsyncEffect, Effect
5
+ from pulse.reactive import REACTIVE_CONTEXT, AsyncEffect, Effect
6
6
 
7
7
 
8
8
  class InlineEffectHookState(HookState):
@@ -33,6 +33,20 @@ class InlineEffectHookState(HookState):
33
33
  if key not in self._seen_this_render:
34
34
  self.effects[key].dispose()
35
35
  del self.effects[key]
36
+ # Remove inline effects from the active render scope to avoid parent cleanup.
37
+ rc = REACTIVE_CONTEXT.get()
38
+ scope = rc.scope
39
+ if scope is None or not scope.effects:
40
+ return
41
+ for key in self._seen_this_render:
42
+ effect = self.effects.get(key)
43
+ if effect is None:
44
+ continue
45
+ try:
46
+ scope.effects.remove(effect)
47
+ effect.parent = None
48
+ except ValueError:
49
+ pass
36
50
 
37
51
  def get_or_create(
38
52
  self,