pulse-framework 0.1.54__py3-none-any.whl → 0.1.56__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.
Files changed (80) hide show
  1. pulse/__init__.py +5 -6
  2. pulse/app.py +144 -57
  3. pulse/channel.py +139 -7
  4. pulse/cli/cmd.py +16 -2
  5. pulse/code_analysis.py +38 -0
  6. pulse/codegen/codegen.py +61 -62
  7. pulse/codegen/templates/route.py +100 -56
  8. pulse/component.py +128 -6
  9. pulse/components/for_.py +30 -4
  10. pulse/components/if_.py +28 -5
  11. pulse/components/react_router.py +61 -3
  12. pulse/context.py +39 -5
  13. pulse/cookies.py +108 -4
  14. pulse/decorators.py +193 -24
  15. pulse/env.py +56 -2
  16. pulse/form.py +198 -5
  17. pulse/helpers.py +7 -1
  18. pulse/hooks/core.py +135 -5
  19. pulse/hooks/effects.py +61 -77
  20. pulse/hooks/init.py +60 -1
  21. pulse/hooks/runtime.py +241 -0
  22. pulse/hooks/setup.py +77 -0
  23. pulse/hooks/stable.py +58 -1
  24. pulse/hooks/state.py +107 -20
  25. pulse/js/__init__.py +41 -25
  26. pulse/js/array.py +9 -6
  27. pulse/js/console.py +15 -12
  28. pulse/js/date.py +9 -6
  29. pulse/js/document.py +5 -2
  30. pulse/js/error.py +7 -4
  31. pulse/js/json.py +9 -6
  32. pulse/js/map.py +8 -5
  33. pulse/js/math.py +9 -6
  34. pulse/js/navigator.py +5 -2
  35. pulse/js/number.py +9 -6
  36. pulse/js/obj.py +16 -13
  37. pulse/js/object.py +9 -6
  38. pulse/js/promise.py +19 -13
  39. pulse/js/pulse.py +28 -25
  40. pulse/js/react.py +190 -44
  41. pulse/js/regexp.py +7 -4
  42. pulse/js/set.py +8 -5
  43. pulse/js/string.py +9 -6
  44. pulse/js/weakmap.py +8 -5
  45. pulse/js/weakset.py +8 -5
  46. pulse/js/window.py +6 -3
  47. pulse/messages.py +5 -0
  48. pulse/middleware.py +147 -76
  49. pulse/plugin.py +76 -5
  50. pulse/queries/client.py +186 -39
  51. pulse/queries/common.py +52 -3
  52. pulse/queries/infinite_query.py +154 -2
  53. pulse/queries/mutation.py +127 -7
  54. pulse/queries/query.py +112 -11
  55. pulse/react_component.py +66 -3
  56. pulse/reactive.py +314 -30
  57. pulse/reactive_extensions.py +106 -26
  58. pulse/render_session.py +304 -173
  59. pulse/request.py +46 -11
  60. pulse/routing.py +140 -4
  61. pulse/serializer.py +71 -0
  62. pulse/state.py +177 -9
  63. pulse/test_helpers.py +15 -0
  64. pulse/transpiler/__init__.py +13 -3
  65. pulse/transpiler/assets.py +66 -0
  66. pulse/transpiler/dynamic_import.py +131 -0
  67. pulse/transpiler/emit_context.py +49 -0
  68. pulse/transpiler/function.py +6 -2
  69. pulse/transpiler/imports.py +33 -27
  70. pulse/transpiler/js_module.py +64 -8
  71. pulse/transpiler/py_module.py +1 -7
  72. pulse/transpiler/transpiler.py +4 -0
  73. pulse/user_session.py +119 -18
  74. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/METADATA +5 -5
  75. pulse_framework-0.1.56.dist-info/RECORD +127 -0
  76. pulse/js/react_dom.py +0 -30
  77. pulse/transpiler/react_component.py +0 -51
  78. pulse_framework-0.1.54.dist-info/RECORD +0 -124
  79. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/WHEEL +0 -0
  80. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/entry_points.txt +0 -0
pulse/reactive.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
2
  import copy
3
3
  import inspect
4
4
  from collections.abc import Awaitable, Callable
5
+ from contextlib import contextmanager
5
6
  from contextvars import ContextVar, Token
6
7
  from typing import (
7
8
  Any,
@@ -26,6 +27,29 @@ P = ParamSpec("P")
26
27
 
27
28
 
28
29
  class Signal(Generic[T]):
30
+ """A reactive value container.
31
+
32
+ Reading registers a dependency; writing notifies observers.
33
+
34
+ Args:
35
+ value: Initial value.
36
+ name: Debug name for the signal.
37
+
38
+ Attributes:
39
+ value: Current value (direct access, no tracking).
40
+ name: Debug name.
41
+ last_change: Epoch when last changed.
42
+
43
+ Example:
44
+
45
+ ```python
46
+ count = Signal(0, name="count")
47
+ print(count()) # 0 (registers dependency)
48
+ count.write(1) # Updates and notifies observers
49
+ print(count.value) # 1 (no dependency tracking)
50
+ ```
51
+ """
52
+
29
53
  value: T
30
54
  name: str | None
31
55
  last_change: int
@@ -38,16 +62,30 @@ class Signal(Generic[T]):
38
62
  self.last_change = -1
39
63
 
40
64
  def read(self) -> T:
65
+ """Read the value, registering a dependency in the current scope.
66
+
67
+ Returns:
68
+ The current value.
69
+ """
41
70
  rc = REACTIVE_CONTEXT.get()
42
71
  if rc.scope is not None:
43
72
  rc.scope.register_dep(self)
44
73
  return self.value
45
74
 
46
75
  def __call__(self) -> T:
76
+ """Alias for read().
77
+
78
+ Returns:
79
+ The current value.
80
+ """
47
81
  return self.read()
48
82
 
49
83
  def unwrap(self) -> T:
50
- """Return the current value while registering subscriptions."""
84
+ """Alias for read().
85
+
86
+ Returns:
87
+ The current value while registering subscriptions.
88
+ """
51
89
  return self.read()
52
90
 
53
91
  def __copy__(self):
@@ -87,6 +125,13 @@ class Signal(Generic[T]):
87
125
  return off
88
126
 
89
127
  def write(self, value: T):
128
+ """Update the value and notify observers.
129
+
130
+ No-op if the new value equals the current value.
131
+
132
+ Args:
133
+ value: The new value to set.
134
+ """
90
135
  if values_equal(value, self.value):
91
136
  return
92
137
  increment_epoch()
@@ -97,6 +142,33 @@ class Signal(Generic[T]):
97
142
 
98
143
 
99
144
  class Computed(Generic[T_co]):
145
+ """A derived value that auto-updates when dependencies change.
146
+
147
+ Lazy evaluation: only recomputes when read and dirty. Throws if a signal
148
+ is written inside the computed function.
149
+
150
+ Args:
151
+ fn: Function computing the value. May optionally accept prev_value
152
+ as first positional argument for incremental computation.
153
+ name: Debug name for the computed.
154
+
155
+ Attributes:
156
+ value: Cached computed value.
157
+ name: Debug name.
158
+ dirty: Whether recompute is needed.
159
+ last_change: Epoch when value last changed.
160
+
161
+ Example:
162
+
163
+ ```python
164
+ count = Signal(5)
165
+ doubled = Computed(lambda: count() * 2)
166
+ print(doubled()) # 10
167
+ count.write(10)
168
+ print(doubled()) # 20
169
+ ```
170
+ """
171
+
100
172
  fn: Callable[..., T_co]
101
173
  name: str | None
102
174
  dirty: bool
@@ -128,6 +200,14 @@ class Computed(Generic[T_co]):
128
200
  )
129
201
 
130
202
  def read(self) -> T_co:
203
+ """Get the computed value, recomputing if dirty, and register a dependency.
204
+
205
+ Returns:
206
+ The computed value.
207
+
208
+ Raises:
209
+ RuntimeError: If circular dependency detected.
210
+ """
131
211
  if self.on_stack:
132
212
  raise RuntimeError("Circular dependency detected")
133
213
 
@@ -141,10 +221,19 @@ class Computed(Generic[T_co]):
141
221
  return self.value
142
222
 
143
223
  def __call__(self) -> T_co:
224
+ """Alias for read().
225
+
226
+ Returns:
227
+ The computed value.
228
+ """
144
229
  return self.read()
145
230
 
146
231
  def unwrap(self) -> T_co:
147
- """Return the current value while registering subscriptions."""
232
+ """Alias for read().
233
+
234
+ Returns:
235
+ The computed value while registering subscriptions.
236
+ """
148
237
  return self.read()
149
238
 
150
239
  def __copy__(self):
@@ -259,9 +348,32 @@ AsyncEffectFn = Callable[[], Awaitable[EffectCleanup | None]]
259
348
 
260
349
 
261
350
  class Effect(Disposable):
262
- """
351
+ """Runs a function when dependencies change.
352
+
263
353
  Synchronous effect and base class. Use AsyncEffect for async effects.
264
354
  Both are isinstance(Effect).
355
+
356
+ Args:
357
+ fn: Effect function. May return a cleanup function to run before the
358
+ next execution or on disposal.
359
+ name: Debug name for the effect.
360
+ immediate: If True, run synchronously when scheduled instead of batching.
361
+ lazy: If True, don't run on creation.
362
+ on_error: Error handler for exceptions in the effect function.
363
+ deps: Explicit dependencies (disables auto-tracking).
364
+ interval: Re-run interval in seconds.
365
+
366
+ Example:
367
+
368
+ ```python
369
+ count = Signal(0)
370
+ def log_count():
371
+ print(f"Count: {count()}")
372
+ return lambda: print("Cleanup")
373
+ effect = Effect(log_count)
374
+ count.write(1) # Effect runs after batch flush
375
+ effect.dispose()
376
+ ```
265
377
  """
266
378
 
267
379
  fn: EffectFn
@@ -273,7 +385,7 @@ class Effect(Disposable):
273
385
  _lazy: bool
274
386
  _interval: float | None
275
387
  _interval_handle: asyncio.TimerHandle | None
276
- explicit_deps: bool
388
+ update_deps: bool
277
389
  batch: "Batch | None"
278
390
  paused: bool
279
391
 
@@ -285,6 +397,7 @@ class Effect(Disposable):
285
397
  lazy: bool = False,
286
398
  on_error: Callable[[Exception], None] | None = None,
287
399
  deps: list[Signal[Any] | Computed[Any]] | None = None,
400
+ update_deps: bool | None = None,
288
401
  interval: float | None = None,
289
402
  ):
290
403
  self.fn = fn # type: ignore[assignment]
@@ -298,7 +411,10 @@ class Effect(Disposable):
298
411
  self.last_run = -1
299
412
  self.scope: Scope | None = None
300
413
  self.batch = None
301
- self.explicit_deps = deps is not None
414
+ if deps is None:
415
+ self.update_deps = True if update_deps is None else update_deps
416
+ else:
417
+ self.update_deps = False if update_deps is None else update_deps
302
418
  self.immediate = immediate
303
419
  self._lazy = lazy
304
420
  self._interval = interval
@@ -308,7 +424,7 @@ class Effect(Disposable):
308
424
  if immediate and lazy:
309
425
  raise ValueError("An effect cannot be boht immediate and lazy")
310
426
 
311
- # Register explicit dependencies immediately upon initialization
427
+ # Register seeded/explicit dependencies immediately upon initialization
312
428
  if deps is not None:
313
429
  self.deps = {dep: dep.last_change for dep in deps}
314
430
  for dep in deps:
@@ -331,6 +447,7 @@ class Effect(Disposable):
331
447
 
332
448
  @override
333
449
  def dispose(self):
450
+ """Clean up the effect, run cleanup function, remove from dependencies."""
334
451
  self.cancel(cancel_interval=True)
335
452
  for child in self.children.copy():
336
453
  child.dispose()
@@ -338,7 +455,7 @@ class Effect(Disposable):
338
455
  self.cleanup_fn()
339
456
  for dep in self.deps:
340
457
  dep.obs.remove(self)
341
- if self.parent:
458
+ if self.parent and self in self.parent.children:
342
459
  self.parent.children.remove(self)
343
460
 
344
461
  def _schedule_interval(self):
@@ -362,7 +479,7 @@ class Effect(Disposable):
362
479
  self._interval_handle = None
363
480
 
364
481
  def pause(self):
365
- """Pause the effect - it won't run when dependencies change."""
482
+ """Pause the effect; it won't run when dependencies change."""
366
483
  self.paused = True
367
484
  self.cancel(cancel_interval=True)
368
485
 
@@ -373,6 +490,7 @@ class Effect(Disposable):
373
490
  self.schedule()
374
491
 
375
492
  def schedule(self):
493
+ """Schedule the effect to run in the current batch."""
376
494
  if self.paused:
377
495
  return
378
496
  # Immediate effects run right away when scheduled and do not enter a batch
@@ -423,7 +541,7 @@ class Effect(Disposable):
423
541
  def __call__(self):
424
542
  self.run()
425
543
 
426
- def flush(self) -> None:
544
+ def flush(self):
427
545
  """If scheduled in a batch, remove and run immediately."""
428
546
  if self.batch is not None:
429
547
  self.batch.effects.remove(self)
@@ -447,7 +565,7 @@ class Effect(Disposable):
447
565
  captured_last_changes: dict[Signal[Any] | Computed[Any], int] | None = None,
448
566
  ) -> None:
449
567
  # Apply captured last_change values at the end for explicit deps
450
- if self.explicit_deps:
568
+ if not self.update_deps:
451
569
  assert captured_last_changes is not None
452
570
  for dep, last_change in captured_last_changes.items():
453
571
  self.deps[dep] = last_change
@@ -458,8 +576,7 @@ class Effect(Disposable):
458
576
  child.parent = self
459
577
 
460
578
  prev_deps = set(self.deps)
461
- if not self.explicit_deps:
462
- self.deps = scope.deps
579
+ self.deps = scope.deps
463
580
  new_deps = set(self.deps)
464
581
  add_deps = new_deps - prev_deps
465
582
  remove_deps = prev_deps - new_deps
@@ -476,7 +593,7 @@ class Effect(Disposable):
476
593
 
477
594
  def _copy_kwargs(self) -> dict[str, Any]:
478
595
  deps = None
479
- if self.explicit_deps:
596
+ if not self.update_deps or (self.update_deps and self.runs == 0 and self.deps):
480
597
  deps = list(self.deps.keys())
481
598
  return {
482
599
  "fn": self.fn,
@@ -485,6 +602,7 @@ class Effect(Disposable):
485
602
  "lazy": self._lazy,
486
603
  "on_error": self.on_error,
487
604
  "deps": deps,
605
+ "update_deps": self.update_deps,
488
606
  "interval": self._interval,
489
607
  }
490
608
 
@@ -507,6 +625,7 @@ class Effect(Disposable):
507
625
  return new_effect
508
626
 
509
627
  def run(self):
628
+ """Execute the effect immediately."""
510
629
  with Untrack():
511
630
  try:
512
631
  self._cleanup_before_run()
@@ -518,7 +637,7 @@ class Effect(Disposable):
518
637
  execution_epoch = epoch()
519
638
  # Capture last_change for explicit deps before running
520
639
  captured_last_changes: dict[Signal[Any] | Computed[Any], int] | None = None
521
- if self.explicit_deps:
640
+ if not self.update_deps:
522
641
  captured_last_changes = {dep: dep.last_change for dep in self.deps}
523
642
  with Scope() as scope:
524
643
  # Clear batch *before* running as we may update a signal that causes
@@ -535,8 +654,63 @@ class Effect(Disposable):
535
654
  if self._interval is not None and self._interval_handle is None:
536
655
  self._schedule_interval()
537
656
 
657
+ def set_deps(
658
+ self,
659
+ deps: list[Signal[Any] | Computed[Any]]
660
+ | dict[Signal[Any] | Computed[Any], int],
661
+ *,
662
+ update_deps: bool | None = None,
663
+ ) -> None:
664
+ if update_deps is not None:
665
+ self.update_deps = update_deps
666
+ if isinstance(deps, dict):
667
+ new_deps = dict(deps)
668
+ else:
669
+ new_deps = {dep: dep.last_change for dep in deps}
670
+ prev_deps = set(self.deps)
671
+ new_dep_keys = set(new_deps)
672
+ add_deps = new_dep_keys - prev_deps
673
+ remove_deps = prev_deps - new_dep_keys
674
+ for dep in remove_deps:
675
+ dep.remove_obs(self)
676
+ self.deps = new_deps
677
+ for dep in add_deps:
678
+ dep.add_obs(self)
679
+ for dep, last_seen in self.deps.items():
680
+ if isinstance(dep, Computed):
681
+ if dep.dirty or dep.last_change > last_seen:
682
+ self.schedule()
683
+ break
684
+ continue
685
+ if dep.last_change > last_seen:
686
+ self.schedule()
687
+ break
688
+
689
+ @contextmanager
690
+ def capture_deps(self, update_deps: bool | None = None):
691
+ scope = Scope()
692
+ try:
693
+ with scope:
694
+ yield
695
+ finally:
696
+ self.set_deps(scope.deps, update_deps=update_deps)
697
+
538
698
 
539
699
  class AsyncEffect(Effect):
700
+ """Async version of Effect for coroutine functions.
701
+
702
+ Does not use batching; cancels and restarts on each dependency change.
703
+ The `immediate` parameter is not supported (raises if passed).
704
+
705
+ Args:
706
+ fn: Async effect function returning an awaitable.
707
+ name: Debug name for the effect.
708
+ lazy: If True, don't run on creation.
709
+ on_error: Error handler for exceptions in the effect function.
710
+ deps: Explicit dependencies (disables auto-tracking).
711
+ interval: Re-run interval in seconds.
712
+ """
713
+
540
714
  fn: AsyncEffectFn # pyright: ignore[reportIncompatibleMethodOverride]
541
715
  batch: None # pyright: ignore[reportIncompatibleVariableOverride]
542
716
  _task: asyncio.Task[None] | None
@@ -549,6 +723,7 @@ class AsyncEffect(Effect):
549
723
  lazy: bool = False,
550
724
  on_error: Callable[[Exception], None] | None = None,
551
725
  deps: list[Signal[Any] | Computed[Any]] | None = None,
726
+ update_deps: bool | None = None,
552
727
  interval: float | None = None,
553
728
  ):
554
729
  # Track an async task when running async effects
@@ -561,6 +736,7 @@ class AsyncEffect(Effect):
561
736
  lazy=lazy,
562
737
  on_error=on_error,
563
738
  deps=deps,
739
+ update_deps=update_deps,
564
740
  interval=interval,
565
741
  )
566
742
 
@@ -595,9 +771,10 @@ class AsyncEffect(Effect):
595
771
 
596
772
  @override
597
773
  def run(self) -> asyncio.Task[Any]: # pyright: ignore[reportIncompatibleMethodOverride]
598
- """
599
- Run the async effect immediately, cancelling any previous run.
600
- Returns the asyncio.Task.
774
+ """Start the async effect, cancelling any previous run.
775
+
776
+ Returns:
777
+ The asyncio.Task running the effect.
601
778
  """
602
779
  execution_epoch = epoch()
603
780
 
@@ -620,7 +797,7 @@ class AsyncEffect(Effect):
620
797
  captured_last_changes: dict[Signal[Any] | Computed[Any], int] | None = (
621
798
  None
622
799
  )
623
- if self.explicit_deps:
800
+ if not self.update_deps:
624
801
  captured_last_changes = {dep: dep.last_change for dep in self.deps}
625
802
 
626
803
  with Scope() as scope:
@@ -669,10 +846,10 @@ class AsyncEffect(Effect):
669
846
  self._cancel_interval()
670
847
 
671
848
  async def wait(self) -> None:
672
- """
673
- Wait until the completion of the current task if it's already running.
674
- Does not start a new task if none is running.
675
- If the task is cancelled while waiting, waits for a new task if one is started.
849
+ """Wait for the current task to complete.
850
+
851
+ Does not start a new task if none is running. If the task is cancelled
852
+ while waiting, waits for a new task if one is started.
676
853
  """
677
854
  while True:
678
855
  if self._task is None or self._task.done():
@@ -702,30 +879,61 @@ class AsyncEffect(Effect):
702
879
  self.cleanup_fn()
703
880
  for dep in self.deps:
704
881
  dep.obs.remove(self)
705
- if self.parent:
882
+ if self.parent and self in self.parent.children:
706
883
  self.parent.children.remove(self)
707
884
 
708
885
 
709
886
  class Batch:
887
+ """Groups reactive updates to run effects once after all writes.
888
+
889
+ By default, effects are scheduled in a global batch that flushes on the
890
+ next event loop iteration. Use as a context manager to create an explicit
891
+ batch that flushes on exit.
892
+
893
+ Args:
894
+ effects: Initial list of effects to schedule.
895
+ name: Debug name for the batch.
896
+
897
+ Example:
898
+
899
+ ```python
900
+ count = Signal(0)
901
+ with Batch() as batch:
902
+ count.write(1)
903
+ count.write(2)
904
+ count.write(3)
905
+ # Effects run once here with final value 3
906
+ ```
907
+ """
908
+
710
909
  name: str | None
910
+ flush_id: int
711
911
 
712
912
  def __init__(
713
913
  self, effects: list[Effect] | None = None, name: str | None = None
714
914
  ) -> None:
715
915
  self.effects: list[Effect] = effects or []
716
916
  self.name = name
917
+ self.flush_id = 0
717
918
  self._token: "Token[ReactiveContext] | None" = None
718
919
 
719
920
  def register_effect(self, effect: Effect):
921
+ """Add an effect to run when the batch flushes.
922
+
923
+ Args:
924
+ effect: The effect to schedule.
925
+ """
720
926
  if effect not in self.effects:
721
927
  self.effects.append(effect)
722
928
 
723
929
  def flush(self):
930
+ """Run all scheduled effects."""
724
931
  token = None
725
932
  rc = REACTIVE_CONTEXT.get()
726
933
  if rc.batch is not self:
727
934
  token = REACTIVE_CONTEXT.set(ReactiveContext(rc.epoch, self, rc.scope))
728
935
 
936
+ self.flush_id += 1
729
937
  MAX_ITERS = 10000
730
938
  iters = 0
731
939
 
@@ -821,9 +1029,27 @@ class Epoch:
821
1029
  self.current = current
822
1030
 
823
1031
 
824
- # Used to track dependencies and effects created within a certain function or
825
- # context.
826
1032
  class Scope:
1033
+ """Tracks dependencies and effects created within a context.
1034
+
1035
+ Use as a context manager to capture which signals/computeds are read
1036
+ and which effects are created.
1037
+
1038
+ Attributes:
1039
+ deps: Tracked dependencies mapping Signal/Computed to last_change epoch.
1040
+ effects: Effects created in this scope.
1041
+
1042
+ Example:
1043
+
1044
+ ```python
1045
+ with Scope() as scope:
1046
+ value = signal() # Dependency tracked
1047
+ effect = Effect(fn) # Effect registered
1048
+ print(scope.deps) # {signal: last_change}
1049
+ print(scope.effects) # [effect]
1050
+ ```
1051
+ """
1052
+
827
1053
  def __init__(self):
828
1054
  # Dict preserves insertion order. Maps dependency -> last_change
829
1055
  self.deps: dict[Signal[Any] | Computed[Any], int] = {}
@@ -857,11 +1083,49 @@ class Scope:
857
1083
  return False
858
1084
 
859
1085
 
860
- class Untrack(Scope): ...
1086
+ class Untrack(Scope):
1087
+ """A scope that disables dependency tracking.
1088
+
1089
+ Use as a context manager to read signals without registering dependencies.
1090
+
1091
+ Example:
1092
+
1093
+ ```python
1094
+ with Untrack():
1095
+ value = signal() # No dependency registered
1096
+ ```
1097
+ """
1098
+
1099
+ ...
861
1100
 
862
1101
 
863
- # --- Reactive Context (composite of epoch, batch, scope) ---
864
1102
  class ReactiveContext:
1103
+ """Composite context holding epoch, batch, and scope.
1104
+
1105
+ Use as a context manager to set up a complete reactive environment.
1106
+
1107
+ Args:
1108
+ epoch: Global version counter. Defaults to a new Epoch.
1109
+ batch: Current batch for effect scheduling. Defaults to GlobalBatch.
1110
+ scope: Current scope for dependency tracking.
1111
+ on_effect_error: Global effect error handler.
1112
+
1113
+ Attributes:
1114
+ epoch: Global version counter.
1115
+ batch: Current batch for effect scheduling.
1116
+ scope: Current scope for dependency tracking.
1117
+ on_effect_error: Global effect error handler.
1118
+
1119
+ Example:
1120
+
1121
+ ```python
1122
+ ctx = ReactiveContext()
1123
+ with ctx:
1124
+ # All reactive operations use this context
1125
+ pass
1126
+ ```
1127
+ """
1128
+
865
1129
  epoch: Epoch
866
1130
  batch: Batch
867
1131
  scope: Scope | None
@@ -902,11 +1166,20 @@ class ReactiveContext:
902
1166
  return False
903
1167
 
904
1168
 
905
- def epoch():
1169
+ def epoch() -> int:
1170
+ """Get the current reactive epoch (version counter).
1171
+
1172
+ Returns:
1173
+ The current epoch value.
1174
+ """
906
1175
  return REACTIVE_CONTEXT.get().get_epoch()
907
1176
 
908
1177
 
909
- def increment_epoch():
1178
+ def increment_epoch() -> None:
1179
+ """Increment the reactive epoch.
1180
+
1181
+ Called automatically on signal writes.
1182
+ """
910
1183
  return REACTIVE_CONTEXT.get().increment_epoch()
911
1184
 
912
1185
 
@@ -917,7 +1190,18 @@ REACTIVE_CONTEXT: ContextVar[ReactiveContext] = ContextVar(
917
1190
  )
918
1191
 
919
1192
 
920
- def flush_effects():
1193
+ def flush_effects() -> None:
1194
+ """Flush the current batch, running all scheduled effects.
1195
+
1196
+ Example:
1197
+
1198
+ ```python
1199
+ count = Signal(0)
1200
+ Effect(lambda: print(count()))
1201
+ count.write(1)
1202
+ flush_effects() # Prints: 1
1203
+ ```
1204
+ """
921
1205
  REACTIVE_CONTEXT.get().batch.flush()
922
1206
 
923
1207