pulse-framework 0.1.74__py3-none-any.whl → 0.1.76__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/render_session.py CHANGED
@@ -6,6 +6,7 @@ from asyncio import iscoroutine
6
6
  from collections.abc import Awaitable, Callable
7
7
  from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
8
8
 
9
+ from pulse.channel import Channel
9
10
  from pulse.context import PulseContext
10
11
  from pulse.hooks.runtime import NotFoundInterrupt, RedirectInterrupt
11
12
  from pulse.messages import (
@@ -257,6 +258,8 @@ class RenderSession:
257
258
  _send_message: Callable[[ServerMessage], Any] | None
258
259
  _pending_api: dict[str, asyncio.Future[dict[str, Any]]]
259
260
  _pending_js_results: dict[str, asyncio.Future[Any]]
261
+ _ref_channel: Channel | None
262
+ _ref_channels_by_route: dict[str, Channel]
260
263
  _global_states: dict[str, State]
261
264
  _global_queue: list[ServerMessage]
262
265
  _tasks: TaskRegistry
@@ -290,6 +293,8 @@ class RenderSession:
290
293
  self.forms = FormRegistry(self)
291
294
  self._pending_api = {}
292
295
  self._pending_js_results = {}
296
+ self._ref_channel = None
297
+ self._ref_channels_by_route = {}
293
298
  self._tasks = TaskRegistry(name=f"render:{id}")
294
299
  self._timers = TimerRegistry(tasks=self._tasks, name=f"render:{id}")
295
300
  self.query_store = QueryStore()
@@ -479,6 +484,7 @@ class RenderSession:
479
484
  return
480
485
  try:
481
486
  self.route_mounts.pop(path, None)
487
+ self._ref_channels_by_route.pop(path, None)
482
488
  mount.dispose()
483
489
  except Exception as e:
484
490
  self.report_error(path, "unmount", e)
@@ -486,6 +492,7 @@ class RenderSession:
486
492
  def detach(self, path: str, *, timeout: float | None = None):
487
493
  """Client no longer wants updates. Queue briefly, then dispose."""
488
494
  path = ensure_absolute_path(path)
495
+ self._ref_channels_by_route.pop(path, None)
489
496
  mount = self.route_mounts.get(path)
490
497
  if not mount:
491
498
  return
@@ -598,6 +605,8 @@ class RenderSession:
598
605
  if not fut.done():
599
606
  fut.cancel()
600
607
  self._pending_js_results.clear()
608
+ self._ref_channel = None
609
+ self._ref_channels_by_route.clear()
601
610
  # Close any timer that may have been scheduled during cleanup (ex: query GC)
602
611
  self._timers.cancel_all()
603
612
  self._global_queue = []
@@ -619,6 +628,24 @@ class RenderSession:
619
628
  self._global_states[key] = inst
620
629
  return inst
621
630
 
631
+ def get_ref_channel(self) -> Channel:
632
+ ctx = PulseContext.get()
633
+ if ctx.route is None:
634
+ if self._ref_channel is not None and not self._ref_channel.closed:
635
+ return self._ref_channel
636
+ self._ref_channel = self.channels.create(bind_route=False)
637
+ return self._ref_channel
638
+
639
+ route_path = ctx.route.pulse_route.unique_path()
640
+ channel = self._ref_channels_by_route.get(route_path)
641
+ if channel is not None and channel.closed:
642
+ self._ref_channels_by_route.pop(route_path, None)
643
+ channel = None
644
+ if channel is None:
645
+ channel = self.channels.create(bind_route=True)
646
+ self._ref_channels_by_route[route_path] = channel
647
+ return channel
648
+
622
649
  def flush(self):
623
650
  with PulseContext.update(render=self):
624
651
  flush_effects()
pulse/renderer.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any, NamedTuple, TypeAlias, cast
9
9
  from pulse.debounce import Debounced
10
10
  from pulse.helpers import values_equal
11
11
  from pulse.hooks.core import HookContext
12
+ from pulse.refs import RefHandle
12
13
  from pulse.transpiler import Import
13
14
  from pulse.transpiler.function import Constant, JsFunction, JsxFunction
14
15
  from pulse.transpiler.nodes import (
@@ -34,7 +35,7 @@ from pulse.transpiler.vdom import (
34
35
  VDOMPropValue,
35
36
  )
36
37
 
37
- PropValue: TypeAlias = Node | Callable[..., Any] | Debounced[Any, Any]
38
+ PropValue: TypeAlias = Node | Callable[..., Any] | Debounced[Any, Any] | RefHandle[Any]
38
39
 
39
40
  FRAGMENT_TAG = ""
40
41
  MOUNT_PREFIX = "$$"
@@ -405,6 +406,25 @@ class Renderer:
405
406
  updated[key] = value.render()
406
407
  continue
407
408
 
409
+ if isinstance(value, RefHandle):
410
+ if key != "ref":
411
+ raise TypeError("RefHandle can only be used as the 'ref' prop")
412
+ eval_keys.add(key)
413
+ if isinstance(old_value, (Element, PulseNode)):
414
+ unmount_element(old_value)
415
+ if normalized is None:
416
+ normalized = current.copy()
417
+ normalized[key] = value
418
+ if not (
419
+ isinstance(old_value, RefHandle) and values_equal(old_value, value)
420
+ ):
421
+ updated[key] = {
422
+ "__pulse_ref__": {
423
+ "channelId": value.channel_id,
424
+ "refId": value.id,
425
+ }
426
+ }
427
+ continue
408
428
  if isinstance(value, Debounced):
409
429
  eval_keys.add(key)
410
430
  if isinstance(old_value, (Element, PulseNode)):
@@ -499,6 +519,8 @@ def prop_requires_eval(value: PropValue) -> bool:
499
519
  return True
500
520
  if isinstance(value, Expr):
501
521
  return True
522
+ if isinstance(value, RefHandle):
523
+ return True
502
524
  if isinstance(value, Debounced):
503
525
  return True
504
526
  return callable(value)
pulse/transpiler/vdom.py CHANGED
@@ -161,7 +161,18 @@ single sentinel string. Debounced callbacks use "$cb:<delay_ms>" in the wire for
161
161
  """
162
162
 
163
163
 
164
- VDOMPropValue: TypeAlias = "JsonValue | VDOMExpr | VDOMElement | CallbackPlaceholder"
164
+ class PulseRefPayload(TypedDict):
165
+ channelId: str
166
+ refId: str
167
+
168
+
169
+ class PulseRefSpec(TypedDict):
170
+ __pulse_ref__: PulseRefPayload
171
+
172
+
173
+ VDOMPropValue: TypeAlias = (
174
+ "JsonValue | VDOMExpr | VDOMElement | CallbackPlaceholder | PulseRefSpec"
175
+ )
165
176
  """Allowed prop value types.
166
177
 
167
178
  Hot path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.74
3
+ Version: 0.1.76
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: fastapi>=0.128.0
6
6
  Requires-Dist: uvicorn>=0.24.0
@@ -1,7 +1,7 @@
1
- pulse/__init__.py,sha256=GUPD8THGENBgHsZvixIP8wOiEtdEMtIVpr8N8MpuRL4,32675
1
+ pulse/__init__.py,sha256=jFSqmTbDLp07bGVr8N7Pa6k0h7Ipq2pYn_tsrr2Ztu8,32881
2
2
  pulse/_examples.py,sha256=dFuhD2EVXsbvAeexoG57s4VuN4gWLaTMOEMNYvlPm9A,561
3
3
  pulse/app.py,sha256=Bi94rYG-MoldkGa-_CscLMstjTEV8BHVAgDbvapRGzI,36167
4
- pulse/channel.py,sha256=ePpvD2mDbddt_LMxxxDjNRgOLbVi8Ed6TmJFgkrALB0,15790
4
+ pulse/channel.py,sha256=UkImBCIFr5sWdkpB3dFwwFa-nWyEnl1W3EaLv0BRsMU,15845
5
5
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  pulse/cli/cmd.py,sha256=LQK_B6iANOAqcQCM0KMTfRbpqGYRaPDkEBvvaAS3qNI,15985
7
7
  pulse/cli/dependencies.py,sha256=qU-rF7QyP0Rl1Fl0YKQubrGNBzj84BAbH1uUT3ehxik,4283
@@ -33,7 +33,7 @@ pulse/decorators.py,sha256=Lskni9Keqfb-xmUliFQe5x-4AcNqrwdvoh0kuz2fXa0,9958
33
33
  pulse/dom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  pulse/dom/elements.py,sha256=YHXkVpfMAC4-0o61fK-E0LGTOM3KMCtBfpHHAwLx7dw,23241
35
35
  pulse/dom/events.py,sha256=yHioH8Y-b7raOaZ43JuCxk2lUBryUAcDSc-5VhXtiSI,14699
36
- pulse/dom/props.py,sha256=WrPwOYSoJmn-VWxU2KvJC1j64L4tlT8X2JpabK94gYQ,26721
36
+ pulse/dom/props.py,sha256=6F3dE_bShI2WdAVfFG0DbIQom2GcM0iF8B3FBE5bj14,26775
37
37
  pulse/dom/svg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  pulse/dom/tags.py,sha256=U6mKmwB9JAFM6LTESMJcoIejNfnyxIdQo2-TLM5OaZ0,7585
39
39
  pulse/dom/tags.pyi,sha256=0BC7zTh22roPBuMQawL8hgI6IrfN8xJZuDIoKMd4QKc,14393
@@ -48,27 +48,44 @@ pulse/hooks/runtime.py,sha256=ogrm4Prvr9ZNaBb5bLfZBHrzbJuYe1zKpIPkbdnIzsw,12286
48
48
  pulse/hooks/setup.py,sha256=NcQPKnMV5dO0vUsWi4u9c9LB0wqFstrtiPGdvihtGiQ,6872
49
49
  pulse/hooks/stable.py,sha256=hLCOl_oAbgdNwiaWwwUTk7ZsHvMqXFFozFztJZMyGbQ,3627
50
50
  pulse/hooks/state.py,sha256=zRFlcOUdl-SBkJ-EzVXRLrLXzAc4-uRzgH4hD9rM1oU,5251
51
- pulse/js/__init__.py,sha256=tj1A6-eR5WS83UNgHb3Dw23m37oJsEuyV0ezUB6kXbg,3636
52
- pulse/js/__init__.pyi,sha256=WN22WsJB-XFk6auL9zklwG2Kof3zeOsc56A56dJ3MWg,3097
51
+ pulse/js/__init__.py,sha256=csEWvFon_ayAiR6OVQl2wnukaLWLK1_PPodVlcY_EvY,5062
52
+ pulse/js/__init__.pyi,sha256=DCgO3uNh6dQLVd9QD3I-LyiPP9VrtE7MZYYHr-8EvVg,5479
53
53
  pulse/js/_types.py,sha256=F4Go2JtJ2dbxq1fXpc2ablG_nyvhvHzOlZLlEv0VmyU,7421
54
+ pulse/js/abort_controller.py,sha256=nKiUgS_uy2Rgh-MNG7fcGIm8Qd6g7hNV_UVk7OgK3qg,1161
54
55
  pulse/js/array.py,sha256=_tC6QZlflWCXOXXUMMtowM3UK7iDWAtFM8BKqR5rjKk,8883
56
+ pulse/js/array_buffer.py,sha256=-iuiSZuv-rx3d0jpQtQYZLXZ2nLuMF93QrQBO-HXv5o,4100
57
+ pulse/js/blob.py,sha256=XiI4g_fKaKtB8OPzS6D4rFG0fRBZlCl9kbO7a5IFPPk,981
55
58
  pulse/js/console.py,sha256=A-GNKEnPby10gdcTdYsBPVfz4m94PYzTXRwGhfaPRpc,1775
59
+ pulse/js/crypto.py,sha256=Atxtc2oqb4xnW9nQV3ruamQhCAl2O_H2XiECspN1JvI,1245
60
+ pulse/js/custom_event.py,sha256=SxtzPQJwgSXRmesd3SiaVdCGYHsFcrnAiPZA63YuunA,901
56
61
  pulse/js/date.py,sha256=qJjdwupuUtKS95u8N8C8FKMKOIB8qjVMsYA3VYfe-tA,3363
57
62
  pulse/js/document.py,sha256=SBinVGfb05jFpeyxAE0yk5Z__dkdW_mFsTI-rvgc-S8,3004
63
+ pulse/js/dom_parser.py,sha256=0qRlAHVr1gDphCPUOSJRcZR94giJF6JmsWOJv4I6p80,818
58
64
  pulse/js/error.py,sha256=v0_DmpN5ESt_CJTrIYfy8980eerjK8mHhQatNV_1M_8,2611
65
+ pulse/js/fetch_api.py,sha256=-4qy5DkQcCeE9CvFMtq-by-WgA7PirgOrmgMflMsluQ,2342
66
+ pulse/js/file.py,sha256=LDoAUXyq82VUULVkIeSjcIj1Vvy2hCi_LRUWh8THyP8,1177
67
+ pulse/js/file_reader.py,sha256=-Y5rfjb2j9tQ-NDX-mWgERC6Alfllh2VvRr6rzXoII0,664
68
+ pulse/js/form_data.py,sha256=_NKZZmD4G4gHsq-QKFMAFwU73ZatGK06p6_oUdQnh_U,1258
69
+ pulse/js/intersection_observer.py,sha256=ZcNV1qyiXjKPOKz5zUQTMZ3oCpfxxSRxLCEDHS_5aKY,1672
70
+ pulse/js/intl.py,sha256=Y_a1YvdQPzOpJAnpPiyM7Ip03L5mh-noChRDeDneVjU,2034
59
71
  pulse/js/json.py,sha256=P8nxOANjIxrzUA1XkBrd0SmNyAGyB3pVXZDPnA1OKks,1908
60
72
  pulse/js/map.py,sha256=bhw75CUMIearH4JACCs9zAffdzfla3Rae7SKGcCLGoc,2243
61
73
  pulse/js/math.py,sha256=OBbMlgoa6ZHLDgmXGNKMj5wYrvroV5ICIx-VsSE5_II,1757
74
+ pulse/js/mutation_observer.py,sha256=7Ai74vHZFkAnVS-OrGYRanY9TpJZmHJ4PrKpz8jmTTU,1831
62
75
  pulse/js/navigator.py,sha256=2QWSr9xyyBfgd76P472qmayXQRYXIEo-b8zLzvfhHfg,1517
63
76
  pulse/js/number.py,sha256=fX2M6hZ5ry2FPsYaHhGlqgO6reBEXw7C-gtu0-8_Zyw,1262
64
77
  pulse/js/obj.py,sha256=8JG9OZZ1CNqAFoMTdYtxWhTmb6zs1BqxC-nLT7KYMF8,2030
65
78
  pulse/js/object.py,sha256=95WvnGWgB-PL-D7l12UgdxNy_fxO5sJXool3Rx5ahUQ,4433
79
+ pulse/js/performance_observer.py,sha256=uletaqnJ6kvJqFSSFtmNo0j--FduK4uZHd8cfRqeN3A,1156
66
80
  pulse/js/promise.py,sha256=vBXcL-U9BuZN-q1jbYhyzQaOL2niDPw4LsD7q7Y_yco,4670
67
81
  pulse/js/pulse.py,sha256=m-LgqwhYygVBj7GzjeO-uo8fK5ThyVe7c3QvOJt_vc0,2962
68
82
  pulse/js/react.py,sha256=eRMrgM8RsoAIn2lcHDoUYas3l4tImLOW51dwmw9AxQU,12057
69
83
  pulse/js/regexp.py,sha256=qO-3nmt7uGN7V_bwimPCN-2RSsPfE6YiY7G1MjoP3YY,1055
84
+ pulse/js/resize_observer.py,sha256=MN0pqnBETmI7bSrWT1bus4_vTZKmJ0jwBoW-cYQWoT4,1162
70
85
  pulse/js/set.py,sha256=omG3g-25GRHxgoKISSB4x-M8UDFlaXtFV9cSIpd5uB0,3017
71
86
  pulse/js/string.py,sha256=VsvDF_ve8R9QIiBdDotLP2KpCKwmpEfGgRQWckOCmHk,813
87
+ pulse/js/text_encoding.py,sha256=5Qvl4XRva9m6tz9NKlv9Nej-O92epMG-y_bu6vCNHwI,1291
88
+ pulse/js/url.py,sha256=-sOthAlPOhJvkXh4hhNLOuWbPKd_ICnG7u--yHDPzY4,2200
72
89
  pulse/js/weakmap.py,sha256=HACZEZ8EZk1xoaCmTXk__opvJLxlJ9_0U3y-01KQkHU,1412
73
90
  pulse/js/weakset.py,sha256=jerMG9ubroR29HvOLIm6lkLxMj-GGWbiE57U9F6zRuQ,1256
74
91
  pulse/js/window.py,sha256=yC1BjyH2jqp1x-CXJCUFta-ASyZ5668ozQ0AmAjZcxA,4097
@@ -89,8 +106,9 @@ pulse/queries/store.py,sha256=iw05_EFpyfiXv5_FV_x4aHtCo00mk0dDPFD461cajcg,3850
89
106
  pulse/react_component.py,sha256=8RLg4Bi7IcjqbnbEnp4hJpy8t1UsE7mG0UR1Q655LDk,2332
90
107
  pulse/reactive.py,sha256=GSh9wSH3THCBjDTafwWttyx7djeKBWV_KqjaKRYUNsA,31393
91
108
  pulse/reactive_extensions.py,sha256=yQ1PpdAh4kMvll7R15T72FOg8NFdG_HGBsGc63dawYk,33754
92
- pulse/render_session.py,sha256=WKWDOqtIjy9n00HxMiViI-pBHw34QOEhLgZap28BCMg,23431
93
- pulse/renderer.py,sha256=a4gTEFZuhAc1V5uTcFFcsOREDg6ZU9-jf4Ic7qLo2CY,16902
109
+ pulse/refs.py,sha256=-6QlzJwJ_lLgGGvJDl6OTOkoA6799TvBGla54ZTH_jA,22889
110
+ pulse/render_session.py,sha256=bujJ0Ch7wUgCqjlq3Z0e8zlg6xpkMThCD-daUSCw-xc,24406
111
+ pulse/renderer.py,sha256=xFjF9Ttv7M74BpHDHpAp32rTFztwyQDZcEWhOFLI5MU,17553
94
112
  pulse/request.py,sha256=N0oFOLiGxpbgSgxznjvu64lG3YyOcZPKC8JFyKx6X7w,6023
95
113
  pulse/requirements.py,sha256=nMnE25Uu-TUuQd88jW7m2xwus6fD-HvXxQ9UNb7OOGc,1254
96
114
  pulse/routing.py,sha256=oRfVaeIrsbDR9yW9BYwxVWV3HZI7wk21yZX69IVADIU,17279
@@ -122,12 +140,12 @@ pulse/transpiler/nodes.py,sha256=ObdCFIEvtKMVRO8iy1hIN4L-vC4yPqRvhPS6E344-bE,526
122
140
  pulse/transpiler/parse.py,sha256=uz_KDnjmjzFSjGtVKRznWg95P0NHM8CafWgvqrqJcOs,1622
123
141
  pulse/transpiler/py_module.py,sha256=um4BYLrbs01bpgv2LEBHTbhXXh8Bs174c3ygv5tHHOg,4410
124
142
  pulse/transpiler/transpiler.py,sha256=bu33-wGNqHGheT_ZqMnQgEARyPG6xyOvuLuixjxIZnI,42761
125
- pulse/transpiler/vdom.py,sha256=Bf1yw10hQl8BXa6rhr5byRa5ua3qgRsVGNgEtQneA2A,6460
143
+ pulse/transpiler/vdom.py,sha256=5ooW9uoWoBEEKBSds27m6Birj3eOuWZ2Qh2nZ4f_kvo,6609
126
144
  pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
145
  pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
128
146
  pulse/user_session.py,sha256=nsnsMgqq2xGJZLpbHRMHUHcLrElMP8WcA4gjGMrcoBk,10208
129
147
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
130
- pulse_framework-0.1.74.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
131
- pulse_framework-0.1.74.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
132
- pulse_framework-0.1.74.dist-info/METADATA,sha256=m4TuEvBPlRud4ARyJi26t3nDDk2gFPvFd4xWi8k2w3k,8299
133
- pulse_framework-0.1.74.dist-info/RECORD,,
148
+ pulse_framework-0.1.76.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
149
+ pulse_framework-0.1.76.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
150
+ pulse_framework-0.1.76.dist-info/METADATA,sha256=NQtUswIWI_6HPwjmn9htpOBclmshDNxcXdPmhggApNQ,8299
151
+ pulse_framework-0.1.76.dist-info/RECORD,,