pulse-framework 0.1.75__py3-none-any.whl → 0.1.77__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.75
3
+ Version: 0.1.77
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
@@ -23,7 +23,7 @@ pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_q
23
23
  pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
24
24
  pulse/component.py,sha256=mY4ZTX7XKXGXAiVwTec1YR3_HOJf6uTdZcxCT_WX5Gs,6230
25
25
  pulse/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- pulse/components/for_.py,sha256=lrt1JHegf4OkBbL9nrMOy7zxmbuD8Kn11x32ZGS72lY,2390
26
+ pulse/components/for_.py,sha256=rBLWaZtY9r_HPx7AcUYHnmCSxkjBI1Wk7bUTm6XoxMs,2377
27
27
  pulse/components/if_.py,sha256=5IOq3R70B-JdI-fvDNYDyAaSEtO8L5OaiqHp-jUn-Kw,2153
28
28
  pulse/components/react_router.py,sha256=Nl6juntLSowFc38q7g_VMdcc4ju6lj8DUhpNR2NuOKQ,2934
29
29
  pulse/context.py,sha256=odTQlOhVRIwNvtatrmPe_Fd8Zk0rMcbcqQHBxvWYH5o,2677
@@ -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,10 +48,11 @@ 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=csEWvFon_ayAiR6OVQl2wnukaLWLK1_PPodVlcY_EvY,5062
52
- pulse/js/__init__.pyi,sha256=DCgO3uNh6dQLVd9QD3I-LyiPP9VrtE7MZYYHr-8EvVg,5479
53
- pulse/js/_types.py,sha256=F4Go2JtJ2dbxq1fXpc2ablG_nyvhvHzOlZLlEv0VmyU,7421
51
+ pulse/js/__init__.py,sha256=Pf2tNypjCIWz3IMj7qGqUlWsBdv74r2R9ZwCVYzbdS0,5182
52
+ pulse/js/__init__.pyi,sha256=GQYs2JBnenPcwzOhOLKXUz6gYvVLRdsPYP3ltgxfc-0,6241
53
+ pulse/js/_types.py,sha256=AZpZGu1_SzxrmzQ0lfzkzfeMaLOjIHVLubBlQdtbr70,7577
54
54
  pulse/js/abort_controller.py,sha256=nKiUgS_uy2Rgh-MNG7fcGIm8Qd6g7hNV_UVk7OgK3qg,1161
55
+ pulse/js/animation.py,sha256=p14FhkYGjvwRFOWmLijVPXsntXbmo_PXAQPcxNEJ7C0,4514
55
56
  pulse/js/array.py,sha256=_tC6QZlflWCXOXXUMMtowM3UK7iDWAtFM8BKqR5rjKk,8883
56
57
  pulse/js/array_buffer.py,sha256=-iuiSZuv-rx3d0jpQtQYZLXZ2nLuMF93QrQBO-HXv5o,4100
57
58
  pulse/js/blob.py,sha256=XiI4g_fKaKtB8OPzS6D4rFG0fRBZlCl9kbO7a5IFPPk,981
@@ -59,7 +60,7 @@ pulse/js/console.py,sha256=A-GNKEnPby10gdcTdYsBPVfz4m94PYzTXRwGhfaPRpc,1775
59
60
  pulse/js/crypto.py,sha256=Atxtc2oqb4xnW9nQV3ruamQhCAl2O_H2XiECspN1JvI,1245
60
61
  pulse/js/custom_event.py,sha256=SxtzPQJwgSXRmesd3SiaVdCGYHsFcrnAiPZA63YuunA,901
61
62
  pulse/js/date.py,sha256=qJjdwupuUtKS95u8N8C8FKMKOIB8qjVMsYA3VYfe-tA,3363
62
- pulse/js/document.py,sha256=SBinVGfb05jFpeyxAE0yk5Z__dkdW_mFsTI-rvgc-S8,3004
63
+ pulse/js/document.py,sha256=0SJD2_1aqMjTF3HgIZq9wBcdJjUvarITb1Oqq0Jp8PM,3260
63
64
  pulse/js/dom_parser.py,sha256=0qRlAHVr1gDphCPUOSJRcZR94giJF6JmsWOJv4I6p80,818
64
65
  pulse/js/error.py,sha256=v0_DmpN5ESt_CJTrIYfy8980eerjK8mHhQatNV_1M_8,2611
65
66
  pulse/js/fetch_api.py,sha256=-4qy5DkQcCeE9CvFMtq-by-WgA7PirgOrmgMflMsluQ,2342
@@ -79,7 +80,7 @@ pulse/js/object.py,sha256=95WvnGWgB-PL-D7l12UgdxNy_fxO5sJXool3Rx5ahUQ,4433
79
80
  pulse/js/performance_observer.py,sha256=uletaqnJ6kvJqFSSFtmNo0j--FduK4uZHd8cfRqeN3A,1156
80
81
  pulse/js/promise.py,sha256=vBXcL-U9BuZN-q1jbYhyzQaOL2niDPw4LsD7q7Y_yco,4670
81
82
  pulse/js/pulse.py,sha256=m-LgqwhYygVBj7GzjeO-uo8fK5ThyVe7c3QvOJt_vc0,2962
82
- pulse/js/react.py,sha256=eRMrgM8RsoAIn2lcHDoUYas3l4tImLOW51dwmw9AxQU,12057
83
+ pulse/js/react.py,sha256=BUfeTOdH3z7n6TXsIJiFWp9euLkbzPmGlrwjAZ6h1Rc,12057
83
84
  pulse/js/regexp.py,sha256=qO-3nmt7uGN7V_bwimPCN-2RSsPfE6YiY7G1MjoP3YY,1055
84
85
  pulse/js/resize_observer.py,sha256=MN0pqnBETmI7bSrWT1bus4_vTZKmJ0jwBoW-cYQWoT4,1162
85
86
  pulse/js/set.py,sha256=omG3g-25GRHxgoKISSB4x-M8UDFlaXtFV9cSIpd5uB0,3017
@@ -106,8 +107,9 @@ pulse/queries/store.py,sha256=iw05_EFpyfiXv5_FV_x4aHtCo00mk0dDPFD461cajcg,3850
106
107
  pulse/react_component.py,sha256=8RLg4Bi7IcjqbnbEnp4hJpy8t1UsE7mG0UR1Q655LDk,2332
107
108
  pulse/reactive.py,sha256=GSh9wSH3THCBjDTafwWttyx7djeKBWV_KqjaKRYUNsA,31393
108
109
  pulse/reactive_extensions.py,sha256=yQ1PpdAh4kMvll7R15T72FOg8NFdG_HGBsGc63dawYk,33754
109
- pulse/render_session.py,sha256=WKWDOqtIjy9n00HxMiViI-pBHw34QOEhLgZap28BCMg,23431
110
- pulse/renderer.py,sha256=a4gTEFZuhAc1V5uTcFFcsOREDg6ZU9-jf4Ic7qLo2CY,16902
110
+ pulse/refs.py,sha256=-6QlzJwJ_lLgGGvJDl6OTOkoA6799TvBGla54ZTH_jA,22889
111
+ pulse/render_session.py,sha256=bujJ0Ch7wUgCqjlq3Z0e8zlg6xpkMThCD-daUSCw-xc,24406
112
+ pulse/renderer.py,sha256=xFjF9Ttv7M74BpHDHpAp32rTFztwyQDZcEWhOFLI5MU,17553
111
113
  pulse/request.py,sha256=N0oFOLiGxpbgSgxznjvu64lG3YyOcZPKC8JFyKx6X7w,6023
112
114
  pulse/requirements.py,sha256=nMnE25Uu-TUuQd88jW7m2xwus6fD-HvXxQ9UNb7OOGc,1254
113
115
  pulse/routing.py,sha256=oRfVaeIrsbDR9yW9BYwxVWV3HZI7wk21yZX69IVADIU,17279
@@ -139,12 +141,12 @@ pulse/transpiler/nodes.py,sha256=ObdCFIEvtKMVRO8iy1hIN4L-vC4yPqRvhPS6E344-bE,526
139
141
  pulse/transpiler/parse.py,sha256=uz_KDnjmjzFSjGtVKRznWg95P0NHM8CafWgvqrqJcOs,1622
140
142
  pulse/transpiler/py_module.py,sha256=um4BYLrbs01bpgv2LEBHTbhXXh8Bs174c3ygv5tHHOg,4410
141
143
  pulse/transpiler/transpiler.py,sha256=bu33-wGNqHGheT_ZqMnQgEARyPG6xyOvuLuixjxIZnI,42761
142
- pulse/transpiler/vdom.py,sha256=Bf1yw10hQl8BXa6rhr5byRa5ua3qgRsVGNgEtQneA2A,6460
144
+ pulse/transpiler/vdom.py,sha256=5ooW9uoWoBEEKBSds27m6Birj3eOuWZ2Qh2nZ4f_kvo,6609
143
145
  pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
146
  pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
145
147
  pulse/user_session.py,sha256=nsnsMgqq2xGJZLpbHRMHUHcLrElMP8WcA4gjGMrcoBk,10208
146
148
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
147
- pulse_framework-0.1.75.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
148
- pulse_framework-0.1.75.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
149
- pulse_framework-0.1.75.dist-info/METADATA,sha256=Y2M3HpMari75ZADpqGSxEfyP5YGCnRS2AkZ3tE3lmpA,8299
150
- pulse_framework-0.1.75.dist-info/RECORD,,
149
+ pulse_framework-0.1.77.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
150
+ pulse_framework-0.1.77.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
151
+ pulse_framework-0.1.77.dist-info/METADATA,sha256=8xfoFxbwvqbwJzkm4dCNS3lz4xbSmfJLNM6ZnVHLSKI,8299
152
+ pulse_framework-0.1.77.dist-info/RECORD,,