pulse-framework 0.1.70__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.
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/PKG-INFO +3 -3
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/pyproject.toml +3 -3
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/__init__.py +7 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/app.py +27 -24
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/cmd.py +1 -1
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/folder_lock.py +25 -6
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/templates/layout.py +3 -1
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/effects.py +15 -1
- pulse_framework-0.1.72/src/pulse/proxy.py +783 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/client.py +64 -56
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/common.py +66 -3
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/infinite_query.py +59 -18
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/query.py +30 -11
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/store.py +13 -11
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/render_session.py +5 -2
- pulse_framework-0.1.70/src/pulse/proxy.py +0 -249
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/README.md +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/code_analysis.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/component.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/forms.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/hooks/state.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/obj.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/pulse.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/react.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/mutation.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/requirements.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/scheduling.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/test_helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/assets.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/dynamic_import.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/emit_context.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/function.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/imports.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/js_module.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/nodes.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/py_module.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/transpiler.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/transpiler/vdom.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.72}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.70 → 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,14 +1322,21 @@ 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
|
|
1328
1331
|
from pulse.queries.common import ActionError as ActionError
|
|
1329
1332
|
from pulse.queries.common import ActionResult as ActionResult
|
|
1330
1333
|
from pulse.queries.common import ActionSuccess as ActionSuccess
|
|
1334
|
+
from pulse.queries.common import Key as Key
|
|
1331
1335
|
from pulse.queries.common import QueryKey as QueryKey
|
|
1336
|
+
from pulse.queries.common import QueryKeys as QueryKeys
|
|
1332
1337
|
from pulse.queries.common import QueryStatus as QueryStatus
|
|
1338
|
+
from pulse.queries.common import keys as keys
|
|
1339
|
+
from pulse.queries.common import normalize_key as normalize_key
|
|
1333
1340
|
from pulse.queries.infinite_query import infinite_query as infinite_query
|
|
1334
1341
|
from pulse.queries.mutation import mutation as mutation
|
|
1335
1342
|
from pulse.queries.protocol import QueryResult as QueryResult
|
|
@@ -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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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]
|
|
@@ -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
|
|
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__(
|
|
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
|
-
|
|
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,
|