pulse-framework 0.1.53__py3-none-any.whl → 0.1.54__py3-none-any.whl
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/__init__.py +3 -3
- pulse/app.py +34 -20
- pulse/components/for_.py +17 -2
- pulse/cookies.py +38 -2
- pulse/env.py +4 -4
- pulse/hooks/init.py +174 -14
- pulse/hooks/state.py +105 -0
- pulse/js/__init__.py +12 -9
- pulse/js/obj.py +79 -0
- pulse/js/pulse.py +112 -0
- pulse/js/react.py +350 -0
- pulse/js/react_dom.py +30 -0
- pulse/messages.py +13 -13
- pulse/proxy.py +18 -5
- pulse/render_session.py +282 -266
- pulse/renderer.py +36 -73
- pulse/serializer.py +5 -2
- pulse/transpiler/builtins.py +0 -20
- pulse/transpiler/errors.py +29 -11
- pulse/transpiler/function.py +30 -3
- pulse/transpiler/js_module.py +9 -12
- pulse/transpiler/modules/pulse/tags.py +35 -15
- pulse/transpiler/nodes.py +121 -36
- pulse/transpiler/py_module.py +1 -1
- pulse/transpiler/transpiler.py +28 -26
- pulse/user_session.py +10 -0
- pulse_framework-0.1.54.dist-info/METADATA +196 -0
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.54.dist-info}/RECORD +30 -26
- pulse/hooks/states.py +0 -285
- pulse_framework-0.1.53.dist-info/METADATA +0 -18
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.54.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.53.dist-info → pulse_framework-0.1.54.dist-info}/entry_points.txt +0 -0
pulse/js/obj.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JavaScript object literal creation.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from pulse.js import obj
|
|
6
|
+
|
|
7
|
+
# Create plain JS objects (not Maps):
|
|
8
|
+
obj(a=1, b=2) # -> { a: 1, b: 2 }
|
|
9
|
+
|
|
10
|
+
# With spread syntax:
|
|
11
|
+
obj(**base, c=3) # -> { ...base, c: 3 }
|
|
12
|
+
obj(a=1, **base) # -> { a: 1, ...base }
|
|
13
|
+
|
|
14
|
+
# Empty object:
|
|
15
|
+
obj() # -> {}
|
|
16
|
+
|
|
17
|
+
Unlike dict() which transpiles to new Map(), obj() creates plain JavaScript
|
|
18
|
+
object literals. Use this for React props, style objects, and anywhere you
|
|
19
|
+
need a plain JS object.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import TYPE_CHECKING, override
|
|
27
|
+
|
|
28
|
+
from pulse.transpiler.errors import TranspileError
|
|
29
|
+
from pulse.transpiler.nodes import Expr, Object, Spread, spread_dict
|
|
30
|
+
from pulse.transpiler.vdom import VDOMNode
|
|
31
|
+
|
|
32
|
+
# TYPE_CHECKING avoids import cycle: Transpiler -> nodes -> Expr -> obj -> Transpiler
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from pulse.transpiler.transpiler import Transpiler
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(slots=True)
|
|
38
|
+
class ObjTransformer(Expr):
|
|
39
|
+
"""Transformer for obj() with **spread support.
|
|
40
|
+
|
|
41
|
+
obj(key=value, ...) -> { key: value, ... }
|
|
42
|
+
obj(**base, key=value) -> { ...base, key: value }
|
|
43
|
+
|
|
44
|
+
Creates a plain JavaScript object literal.
|
|
45
|
+
Use this instead of dict() when you need a plain object (e.g., for React props).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
def emit(self, out: list[str]) -> None:
|
|
50
|
+
raise TypeError("obj cannot be emitted directly - must be called")
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def render(self) -> VDOMNode:
|
|
54
|
+
raise TypeError("obj cannot be rendered - must be called")
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def transpile_call(
|
|
58
|
+
self,
|
|
59
|
+
args: list[ast.expr],
|
|
60
|
+
keywords: list[ast.keyword],
|
|
61
|
+
ctx: Transpiler,
|
|
62
|
+
) -> Expr:
|
|
63
|
+
if args:
|
|
64
|
+
raise TranspileError("obj() only accepts keyword arguments")
|
|
65
|
+
|
|
66
|
+
props: list[tuple[str, Expr] | Spread] = []
|
|
67
|
+
for kw in keywords:
|
|
68
|
+
if kw.arg is None:
|
|
69
|
+
# **spread syntax
|
|
70
|
+
props.append(spread_dict(ctx.emit_expr(kw.value)))
|
|
71
|
+
else:
|
|
72
|
+
# key=value
|
|
73
|
+
props.append((kw.arg, ctx.emit_expr(kw.value)))
|
|
74
|
+
|
|
75
|
+
return Object(props)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Create singleton instance for use as a callable
|
|
79
|
+
obj = ObjTransformer()
|
pulse/js/pulse.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pulse UI client bindings for channel communication.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from pulse.js.pulse import usePulseChannel, ChannelBridge, PulseChannelResetError
|
|
6
|
+
|
|
7
|
+
@ps.javascript(jsx=True)
|
|
8
|
+
def MyChannelComponent(*, channel_id: str):
|
|
9
|
+
bridge = usePulseChannel(channel_id)
|
|
10
|
+
|
|
11
|
+
# Subscribe to events
|
|
12
|
+
useEffect(
|
|
13
|
+
lambda: bridge.on("server:notify", lambda payload: console.log(payload)),
|
|
14
|
+
[bridge],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Emit events to server
|
|
18
|
+
def send_ping():
|
|
19
|
+
bridge.emit("client:ping", {"message": "hello"})
|
|
20
|
+
|
|
21
|
+
# Make requests to server
|
|
22
|
+
async def send_request():
|
|
23
|
+
response = await bridge.request("client:request", {"data": 123})
|
|
24
|
+
console.log(response)
|
|
25
|
+
|
|
26
|
+
return ps.div()[
|
|
27
|
+
ps.button(onClick=send_ping)["Send Ping"],
|
|
28
|
+
ps.button(onClick=send_request)["Send Request"],
|
|
29
|
+
]
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from collections.abc import Awaitable as _Awaitable
|
|
33
|
+
from collections.abc import Callable as _Callable
|
|
34
|
+
from typing import Any as _Any
|
|
35
|
+
from typing import TypeVar as _TypeVar
|
|
36
|
+
|
|
37
|
+
from pulse.transpiler.js_module import JsModule
|
|
38
|
+
|
|
39
|
+
T = _TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PulseChannelResetError(Exception):
|
|
43
|
+
"""Error raised when a channel is closed or reset."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ChannelBridge:
|
|
49
|
+
"""A bridge for bidirectional communication between client and server.
|
|
50
|
+
|
|
51
|
+
Provides methods for emitting events, making requests, and subscribing
|
|
52
|
+
to server events on a specific channel.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def id(self) -> str:
|
|
57
|
+
"""The unique channel identifier."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def emit(self, event: str, payload: _Any = None) -> None:
|
|
61
|
+
"""Emit an event to the server.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
event: The event name to emit.
|
|
65
|
+
payload: Optional data to send with the event.
|
|
66
|
+
"""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def request(self, event: str, payload: _Any = None) -> _Awaitable[_Any]:
|
|
70
|
+
"""Make a request to the server and await a response.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
event: The event name to send.
|
|
74
|
+
payload: Optional data to send with the request.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A Promise that resolves with the server's response.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def on(self, event: str, handler: _Callable[[_Any], _Any]) -> _Callable[[], None]:
|
|
82
|
+
"""Subscribe to events from the server.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
event: The event name to listen for.
|
|
86
|
+
handler: A callback function that receives the event payload.
|
|
87
|
+
May be sync or async. For request events, the return value
|
|
88
|
+
is sent back to the server.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A cleanup function that unsubscribes the handler.
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def usePulseChannel(channel_id: str) -> ChannelBridge:
|
|
97
|
+
"""React hook to connect to a Pulse channel.
|
|
98
|
+
|
|
99
|
+
Must be called from within a React component. The channel connection
|
|
100
|
+
is automatically managed based on component lifecycle.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
channel_id: The unique identifier for the channel to connect to.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A ChannelBridge instance for interacting with the channel.
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Register as a JS module with named imports from pulse-ui-client
|
|
112
|
+
JsModule.register(name="pulse", src="pulse-ui-client", values="named_import")
|
pulse/js/react.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JavaScript React module.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from pulse.js.react import useState, useEffect, useRef
|
|
6
|
+
state, setState = useState(0) # -> const [state, setState] = useState(0)
|
|
7
|
+
useEffect(lambda: print("hi"), []) # -> useEffect(() => console.log("hi"), [])
|
|
8
|
+
ref = useRef(None) # -> const ref = useRef(null)
|
|
9
|
+
|
|
10
|
+
# Also available as namespace:
|
|
11
|
+
import pulse.js.react as React
|
|
12
|
+
React.useState(0) # -> React.useState(0)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable as _Callable
|
|
16
|
+
from typing import Any as _Any
|
|
17
|
+
from typing import Protocol as _Protocol
|
|
18
|
+
from typing import TypeVar as _TypeVar
|
|
19
|
+
|
|
20
|
+
from pulse.transpiler.js_module import JsModule
|
|
21
|
+
|
|
22
|
+
# Type variables for hooks
|
|
23
|
+
T = _TypeVar("T")
|
|
24
|
+
T_co = _TypeVar("T_co", covariant=True)
|
|
25
|
+
T_contra = _TypeVar("T_contra", contravariant=True)
|
|
26
|
+
S = _TypeVar("S")
|
|
27
|
+
A = _TypeVar("A")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# React Types
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RefObject(_Protocol[T_co]):
|
|
36
|
+
"""Type for useRef return value."""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def current(self) -> T_co: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MutableRefObject(_Protocol[T]):
|
|
43
|
+
"""Type for useRef return value with mutable current."""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def current(self) -> T: ...
|
|
47
|
+
|
|
48
|
+
@current.setter
|
|
49
|
+
def current(self, value: T) -> None: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Dispatch(_Protocol[T_contra]):
|
|
53
|
+
"""Type for setState/dispatch functions."""
|
|
54
|
+
|
|
55
|
+
def __call__(self, action: T_contra, /) -> None: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TransitionStartFunction(_Protocol):
|
|
59
|
+
"""Type for startTransition callback."""
|
|
60
|
+
|
|
61
|
+
def __call__(self, callback: _Callable[[], None], /) -> None: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Context(_Protocol[T_co]):
|
|
65
|
+
"""Type for React Context."""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def Provider(self) -> _Any: ...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def Consumer(self) -> _Any: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReactNode(_Protocol):
|
|
75
|
+
"""Type for React children."""
|
|
76
|
+
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ReactElement(_Protocol):
|
|
81
|
+
"""Type for React element."""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def type(self) -> _Any: ...
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def props(self) -> _Any: ...
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def key(self) -> str | None: ...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# State Hooks
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def useState(
|
|
99
|
+
initial_state: S | _Callable[[], S],
|
|
100
|
+
) -> tuple[S, Dispatch[S | _Callable[[S], S]]]:
|
|
101
|
+
"""Returns a stateful value and a function to update it.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
count, set_count = useState(0)
|
|
105
|
+
set_count(count + 1)
|
|
106
|
+
set_count(lambda prev: prev + 1)
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def useReducer(
|
|
112
|
+
reducer: _Callable[[S, A], S],
|
|
113
|
+
initial_arg: S,
|
|
114
|
+
init: _Callable[[S], S] | None = None,
|
|
115
|
+
) -> tuple[S, Dispatch[A]]:
|
|
116
|
+
"""An alternative to useState for complex state logic.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
def reducer(state, action):
|
|
120
|
+
if action['type'] == 'increment':
|
|
121
|
+
return {'count': state['count'] + 1}
|
|
122
|
+
return state
|
|
123
|
+
|
|
124
|
+
state, dispatch = useReducer(reducer, {'count': 0})
|
|
125
|
+
dispatch({'type': 'increment'})
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# =============================================================================
|
|
131
|
+
# Effect Hooks
|
|
132
|
+
# =============================================================================
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def useEffect(
|
|
136
|
+
effect: _Callable[[], None | _Callable[[], None]],
|
|
137
|
+
deps: list[_Any] | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Accepts a function that contains imperative, possibly effectful code.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
useEffect(lambda: print("mounted"), [])
|
|
143
|
+
useEffect(lambda: (print("update"), lambda: print("cleanup"))[-1], [dep])
|
|
144
|
+
"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def useLayoutEffect(
|
|
149
|
+
effect: _Callable[[], None | _Callable[[], None]],
|
|
150
|
+
deps: list[_Any] | None = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Like useEffect, but fires synchronously after all DOM mutations.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
useLayoutEffect(lambda: measure_element(), [])
|
|
156
|
+
"""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def useInsertionEffect(
|
|
161
|
+
effect: _Callable[[], None | _Callable[[], None]],
|
|
162
|
+
deps: list[_Any] | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Like useLayoutEffect, but fires before any DOM mutations.
|
|
165
|
+
Use for CSS-in-JS libraries.
|
|
166
|
+
"""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# Ref Hooks
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def useRef(initial_value: T) -> MutableRefObject[T]:
|
|
176
|
+
"""Returns a mutable ref object.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
input_ref = useRef(None)
|
|
180
|
+
# In JSX: <input ref={input_ref} />
|
|
181
|
+
input_ref.current.focus()
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def useImperativeHandle(
|
|
187
|
+
ref: RefObject[T] | _Callable[[T | None], None] | None,
|
|
188
|
+
create_handle: _Callable[[], T],
|
|
189
|
+
deps: list[_Any] | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Customizes the instance value exposed to parent components when using ref."""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# =============================================================================
|
|
196
|
+
# Performance Hooks
|
|
197
|
+
# =============================================================================
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def useMemo(factory: _Callable[[], T], deps: list[_Any]) -> T:
|
|
201
|
+
"""Returns a memoized value.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
expensive = useMemo(lambda: compute_expensive(a, b), [a, b])
|
|
205
|
+
"""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def useCallback(callback: T, deps: list[_Any]) -> T:
|
|
210
|
+
"""Returns a memoized callback.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
handle_click = useCallback(lambda e: print(e), [])
|
|
214
|
+
"""
|
|
215
|
+
...
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def useDeferredValue(value: T) -> T:
|
|
219
|
+
"""Defers updating a part of the UI. Returns a deferred version of the value."""
|
|
220
|
+
...
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def useTransition() -> tuple[bool, TransitionStartFunction]:
|
|
224
|
+
"""Returns a stateful value for pending state and a function to start transition.
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
is_pending, start_transition = useTransition()
|
|
228
|
+
start_transition(lambda: set_state(new_value))
|
|
229
|
+
"""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# =============================================================================
|
|
234
|
+
# Context Hooks
|
|
235
|
+
# =============================================================================
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def useContext(context: Context[T]) -> T:
|
|
239
|
+
"""Returns the current context value for the given context.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
theme = useContext(ThemeContext)
|
|
243
|
+
"""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# =============================================================================
|
|
248
|
+
# Other Hooks
|
|
249
|
+
# =============================================================================
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def useId() -> str:
|
|
253
|
+
"""Generates a unique ID that is stable across server and client.
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
id = useId()
|
|
257
|
+
# <label htmlFor={id}>Name</label>
|
|
258
|
+
# <input id={id} />
|
|
259
|
+
"""
|
|
260
|
+
...
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def useDebugValue(value: T, format_fn: _Callable[[T], _Any] | None = None) -> None:
|
|
264
|
+
"""Displays a label in React DevTools for custom hooks."""
|
|
265
|
+
...
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def useSyncExternalStore(
|
|
269
|
+
subscribe: _Callable[[_Callable[[], None]], _Callable[[], None]],
|
|
270
|
+
get_snapshot: _Callable[[], T],
|
|
271
|
+
get_server_snapshot: _Callable[[], T] | None = None,
|
|
272
|
+
) -> T:
|
|
273
|
+
"""Subscribe to an external store.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
width = useSyncExternalStore(
|
|
277
|
+
subscribe_to_resize,
|
|
278
|
+
lambda: window.innerWidth
|
|
279
|
+
)
|
|
280
|
+
"""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# =============================================================================
|
|
285
|
+
# React Components and Elements
|
|
286
|
+
# =============================================================================
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def createElement(
|
|
290
|
+
type: _Any,
|
|
291
|
+
props: dict[str, _Any] | None = None,
|
|
292
|
+
*children: _Any,
|
|
293
|
+
) -> ReactElement:
|
|
294
|
+
"""Creates a React element."""
|
|
295
|
+
...
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cloneElement(
|
|
299
|
+
element: ReactElement,
|
|
300
|
+
props: dict[str, _Any] | None = None,
|
|
301
|
+
*children: _Any,
|
|
302
|
+
) -> ReactElement:
|
|
303
|
+
"""Clones and returns a new React element."""
|
|
304
|
+
...
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def isValidElement(obj: _Any) -> bool:
|
|
308
|
+
"""Checks if the object is a React element."""
|
|
309
|
+
...
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def memo(component: T, are_equal: _Callable[[_Any, _Any], bool] | None = None) -> T:
|
|
313
|
+
"""Memoizes a component to skip re-rendering when props are unchanged."""
|
|
314
|
+
...
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def forwardRef(
|
|
318
|
+
render: _Callable[[_Any, _Any], ReactElement | None],
|
|
319
|
+
) -> _Callable[..., ReactElement | None]:
|
|
320
|
+
"""Lets your component expose a DOM node to a parent component with a ref."""
|
|
321
|
+
...
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def lazy(load: _Callable[[], _Any]) -> _Any:
|
|
325
|
+
"""Lets you defer loading a component's code until it is rendered."""
|
|
326
|
+
...
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def createContext(default_value: T) -> Context[T]:
|
|
330
|
+
"""Creates a Context object."""
|
|
331
|
+
...
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# =============================================================================
|
|
335
|
+
# Fragments
|
|
336
|
+
# =============================================================================
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class Fragment:
|
|
340
|
+
"""Lets you group elements without a wrapper node."""
|
|
341
|
+
|
|
342
|
+
...
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# =============================================================================
|
|
346
|
+
# Registration
|
|
347
|
+
# =============================================================================
|
|
348
|
+
|
|
349
|
+
# React is a namespace module where each hook is a named import
|
|
350
|
+
JsModule.register(name="React", src="react", kind="namespace", values="named_import")
|
pulse/js/react_dom.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JavaScript ReactDOM module.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from pulse.js.react_dom import createPortal
|
|
6
|
+
createPortal(children, container) # -> createPortal(children, container)
|
|
7
|
+
|
|
8
|
+
# Also available as namespace:
|
|
9
|
+
import pulse.js.react_dom as ReactDOM
|
|
10
|
+
ReactDOM.createPortal(children, container)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any as _Any
|
|
14
|
+
|
|
15
|
+
# Import types from react module for consistency
|
|
16
|
+
from pulse.js.react import ReactNode as ReactNode
|
|
17
|
+
from pulse.transpiler.js_module import JsModule
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def createPortal(
|
|
21
|
+
children: ReactNode, container: _Any, key: str | None = None
|
|
22
|
+
) -> ReactNode:
|
|
23
|
+
"""Creates a portal to render children into a different DOM subtree."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ReactDOM is a namespace module where each export is a named import
|
|
28
|
+
JsModule.register(
|
|
29
|
+
name="ReactDOM", src="react-dom", kind="namespace", values="named_import"
|
|
30
|
+
)
|
pulse/messages.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Any, Literal, NotRequired, TypedDict
|
|
2
2
|
|
|
3
3
|
from pulse.routing import RouteInfo
|
|
4
|
-
from pulse.transpiler.vdom import VDOM, VDOMOperation
|
|
4
|
+
from pulse.transpiler.vdom import VDOM, VDOMNode, VDOMOperation
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
# ====================
|
|
@@ -80,12 +80,12 @@ class ServerChannelResponseMessage(TypedDict):
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
class ServerJsExecMessage(TypedDict):
|
|
83
|
-
"""Execute JavaScript
|
|
83
|
+
"""Execute JavaScript expression on the client."""
|
|
84
84
|
|
|
85
85
|
type: Literal["js_exec"]
|
|
86
86
|
path: str
|
|
87
87
|
id: str
|
|
88
|
-
|
|
88
|
+
expr: VDOMNode
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
# ====================
|
|
@@ -98,20 +98,20 @@ class ClientCallbackMessage(TypedDict):
|
|
|
98
98
|
args: list[Any]
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
class
|
|
102
|
-
type: Literal["
|
|
101
|
+
class ClientAttachMessage(TypedDict):
|
|
102
|
+
type: Literal["attach"]
|
|
103
103
|
path: str
|
|
104
104
|
routeInfo: RouteInfo
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
class
|
|
108
|
-
type: Literal["
|
|
107
|
+
class ClientUpdateMessage(TypedDict):
|
|
108
|
+
type: Literal["update"]
|
|
109
109
|
path: str
|
|
110
110
|
routeInfo: RouteInfo
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
class
|
|
114
|
-
type: Literal["
|
|
113
|
+
class ClientDetachMessage(TypedDict):
|
|
114
|
+
type: Literal["detach"]
|
|
115
115
|
path: str
|
|
116
116
|
|
|
117
117
|
|
|
@@ -165,9 +165,9 @@ ServerMessage = (
|
|
|
165
165
|
|
|
166
166
|
ClientPulseMessage = (
|
|
167
167
|
ClientCallbackMessage
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
168
|
+
| ClientAttachMessage
|
|
169
|
+
| ClientUpdateMessage
|
|
170
|
+
| ClientDetachMessage
|
|
171
171
|
| ClientApiResultMessage
|
|
172
172
|
| ClientJsResultMessage
|
|
173
173
|
)
|
|
@@ -193,5 +193,5 @@ class Directives(TypedDict):
|
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
class Prerender(TypedDict):
|
|
196
|
-
views: dict[str, ServerInitMessage | None]
|
|
196
|
+
views: dict[str, ServerInitMessage | ServerNavigateToMessage | None]
|
|
197
197
|
directives: Directives
|
pulse/proxy.py
CHANGED
|
@@ -15,6 +15,9 @@ from starlette.responses import PlainTextResponse, Response
|
|
|
15
15
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
16
16
|
from websockets.typing import Subprotocol
|
|
17
17
|
|
|
18
|
+
from pulse.context import PulseContext
|
|
19
|
+
from pulse.cookies import parse_cookie_header
|
|
20
|
+
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
20
23
|
|
|
@@ -108,11 +111,7 @@ class ReactProxy:
|
|
|
108
111
|
)
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
#
|
|
112
|
-
# We'll accept without subprotocol initially, then update if target accepts one
|
|
113
|
-
await websocket.accept()
|
|
114
|
-
|
|
115
|
-
# Connect to target WebSocket server
|
|
114
|
+
# Connect to target WebSocket server first to negotiate subprotocol
|
|
116
115
|
try:
|
|
117
116
|
async with websockets.connect(
|
|
118
117
|
target_url,
|
|
@@ -120,6 +119,9 @@ class ReactProxy:
|
|
|
120
119
|
subprotocols=subprotocols,
|
|
121
120
|
ping_interval=None, # Let the target server handle ping/pong
|
|
122
121
|
) as target_ws:
|
|
122
|
+
# Accept client connection with the negotiated subprotocol
|
|
123
|
+
await websocket.accept(subprotocol=target_ws.subprotocol)
|
|
124
|
+
|
|
123
125
|
# Forward messages bidirectionally
|
|
124
126
|
async def forward_client_to_target():
|
|
125
127
|
try:
|
|
@@ -190,6 +192,17 @@ class ReactProxy:
|
|
|
190
192
|
|
|
191
193
|
# Extract headers, skip host header (will be set by httpx)
|
|
192
194
|
headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
|
|
195
|
+
ctx = PulseContext.get()
|
|
196
|
+
session = ctx.session
|
|
197
|
+
if session is not None:
|
|
198
|
+
session_cookie = session.get_cookie_value(ctx.app.cookie.name)
|
|
199
|
+
if session_cookie:
|
|
200
|
+
existing = parse_cookie_header(headers.get("cookie"))
|
|
201
|
+
if existing.get(ctx.app.cookie.name) != session_cookie:
|
|
202
|
+
existing[ctx.app.cookie.name] = session_cookie
|
|
203
|
+
headers["cookie"] = "; ".join(
|
|
204
|
+
f"{key}={value}" for key, value in existing.items()
|
|
205
|
+
)
|
|
193
206
|
|
|
194
207
|
try:
|
|
195
208
|
# Build request
|