pulse-framework 0.1.63__py3-none-any.whl → 0.1.65__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 +16 -10
- pulse/app.py +30 -11
- pulse/channel.py +3 -3
- pulse/{form.py → forms.py} +2 -2
- pulse/helpers.py +9 -212
- pulse/proxy.py +10 -3
- pulse/queries/client.py +5 -1
- pulse/queries/effect.py +2 -1
- pulse/queries/infinite_query.py +164 -54
- pulse/queries/protocol.py +9 -0
- pulse/queries/query.py +164 -81
- pulse/queries/store.py +10 -2
- pulse/reactive.py +18 -7
- pulse/render_session.py +61 -12
- pulse/scheduling.py +448 -0
- {pulse_framework-0.1.63.dist-info → pulse_framework-0.1.65.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.63.dist-info → pulse_framework-0.1.65.dist-info}/RECORD +19 -18
- {pulse_framework-0.1.63.dist-info → pulse_framework-0.1.65.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.63.dist-info → pulse_framework-0.1.65.dist-info}/entry_points.txt +0 -0
pulse/queries/infinite_query.py
CHANGED
|
@@ -19,8 +19,8 @@ from pulse.context import PulseContext
|
|
|
19
19
|
from pulse.helpers import (
|
|
20
20
|
MISSING,
|
|
21
21
|
Disposable,
|
|
22
|
+
Missing,
|
|
22
23
|
call_flexible,
|
|
23
|
-
later,
|
|
24
24
|
maybe_await,
|
|
25
25
|
)
|
|
26
26
|
from pulse.queries.common import (
|
|
@@ -36,6 +36,7 @@ from pulse.queries.common import (
|
|
|
36
36
|
from pulse.queries.query import RETRY_DELAY_DEFAULT, QueryConfig
|
|
37
37
|
from pulse.reactive import Computed, Effect, Signal, Untrack
|
|
38
38
|
from pulse.reactive_extensions import ReactiveList, unwrap
|
|
39
|
+
from pulse.scheduling import TimerHandleLike, create_task, later
|
|
39
40
|
from pulse.state import InitializableProperty, State
|
|
40
41
|
|
|
41
42
|
T = TypeVar("T")
|
|
@@ -152,6 +153,61 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
152
153
|
)
|
|
153
154
|
return self._observers[0]._fetch_fn # pyright: ignore[reportPrivateUsage]
|
|
154
155
|
|
|
156
|
+
@property
|
|
157
|
+
def has_interval(self) -> bool:
|
|
158
|
+
return self._interval is not None
|
|
159
|
+
|
|
160
|
+
def _select_interval_observer(
|
|
161
|
+
self,
|
|
162
|
+
) -> tuple[float | None, "InfiniteQueryResult[T, TParam] | None"]:
|
|
163
|
+
min_interval: float | None = None
|
|
164
|
+
selected: "InfiniteQueryResult[T, TParam] | None" = None
|
|
165
|
+
|
|
166
|
+
for obs in reversed(self._observers):
|
|
167
|
+
interval = obs._refetch_interval # pyright: ignore[reportPrivateUsage]
|
|
168
|
+
if interval is None:
|
|
169
|
+
continue
|
|
170
|
+
if not obs._enabled.value: # pyright: ignore[reportPrivateUsage]
|
|
171
|
+
continue
|
|
172
|
+
if min_interval is None or interval < min_interval:
|
|
173
|
+
min_interval = interval
|
|
174
|
+
selected = obs
|
|
175
|
+
|
|
176
|
+
return min_interval, selected
|
|
177
|
+
|
|
178
|
+
def _create_interval_effect(self, interval: float) -> Effect:
|
|
179
|
+
def interval_fn():
|
|
180
|
+
observer = self._interval_observer
|
|
181
|
+
if observer is None:
|
|
182
|
+
return
|
|
183
|
+
self.invalidate(fetch_fn=observer._fetch_fn, observer=observer) # pyright: ignore[reportPrivateUsage]
|
|
184
|
+
|
|
185
|
+
return Effect(
|
|
186
|
+
interval_fn,
|
|
187
|
+
name=f"inf_query_interval({self.key})",
|
|
188
|
+
interval=interval,
|
|
189
|
+
immediate=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _update_interval(self) -> None:
|
|
193
|
+
new_interval, new_observer = self._select_interval_observer()
|
|
194
|
+
interval_changed = new_interval != self._interval
|
|
195
|
+
|
|
196
|
+
self._interval = new_interval
|
|
197
|
+
self._interval_observer = new_observer
|
|
198
|
+
|
|
199
|
+
if not interval_changed:
|
|
200
|
+
if self._interval_effect is None and new_interval is not None:
|
|
201
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if self._interval_effect is not None:
|
|
205
|
+
self._interval_effect.dispose()
|
|
206
|
+
self._interval_effect = None
|
|
207
|
+
|
|
208
|
+
if new_interval is not None:
|
|
209
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
210
|
+
|
|
155
211
|
# Reactive state
|
|
156
212
|
pages: ReactiveList[Page[T, TParam]]
|
|
157
213
|
error: Signal[Exception | None]
|
|
@@ -170,7 +226,10 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
170
226
|
_queue_task: asyncio.Task[None] | None
|
|
171
227
|
|
|
172
228
|
_observers: "list[InfiniteQueryResult[T, TParam]]"
|
|
173
|
-
_gc_handle:
|
|
229
|
+
_gc_handle: TimerHandleLike | None
|
|
230
|
+
_interval_effect: Effect | None
|
|
231
|
+
_interval: float | None
|
|
232
|
+
_interval_observer: "InfiniteQueryResult[T, TParam] | None"
|
|
174
233
|
|
|
175
234
|
def __init__(
|
|
176
235
|
self,
|
|
@@ -184,7 +243,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
184
243
|
max_pages: int = 0,
|
|
185
244
|
retries: int = 3,
|
|
186
245
|
retry_delay: float = RETRY_DELAY_DEFAULT,
|
|
187
|
-
initial_data: list[Page[T, TParam]] |
|
|
246
|
+
initial_data: list[Page[T, TParam]] | Missing | None = MISSING,
|
|
188
247
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
189
248
|
gc_time: float = 300.0,
|
|
190
249
|
on_dispose: Callable[[Any], None] | None = None,
|
|
@@ -208,7 +267,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
208
267
|
if initial_data is MISSING:
|
|
209
268
|
initial_pages = []
|
|
210
269
|
else:
|
|
211
|
-
initial_pages = cast(list[Page[T, TParam]], initial_data) or []
|
|
270
|
+
initial_pages = cast(list[Page[T, TParam]] | None, initial_data) or []
|
|
212
271
|
|
|
213
272
|
self.pages = ReactiveList(initial_pages)
|
|
214
273
|
self.error = Signal(None, name=f"inf_query.error({key})")
|
|
@@ -232,6 +291,9 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
232
291
|
self._queue_task = None
|
|
233
292
|
self._observers = []
|
|
234
293
|
self._gc_handle = None
|
|
294
|
+
self._interval_effect = None
|
|
295
|
+
self._interval = None
|
|
296
|
+
self._interval_observer = None
|
|
235
297
|
|
|
236
298
|
# ─────────────────────────────────────────────────────────────────────────
|
|
237
299
|
# Commit functions - update state after pages have been modified
|
|
@@ -326,13 +388,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
326
388
|
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
327
389
|
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
328
390
|
) -> ActionResult[list[Page[T, TParam]]]:
|
|
329
|
-
"""Wait for
|
|
330
|
-
# If no data and loading, enqueue initial fetch (unless already processing)
|
|
331
|
-
if len(self.pages) == 0 and self.status() == "loading":
|
|
332
|
-
if self._queue_task is None or self._queue_task.done():
|
|
333
|
-
# Use provided fetch_fn or fall back to first observer's fetch_fn
|
|
334
|
-
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
335
|
-
self._enqueue(Refetch(fetch_fn=fn, observer=observer))
|
|
391
|
+
"""Wait for any in-flight queue processing to complete."""
|
|
336
392
|
# Wait for any in-progress queue processing
|
|
337
393
|
if self._queue_task and not self._queue_task.done():
|
|
338
394
|
await self._queue_task
|
|
@@ -341,17 +397,31 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
341
397
|
return ActionError(cast(Exception, self.error()))
|
|
342
398
|
return ActionSuccess(list(self.pages))
|
|
343
399
|
|
|
400
|
+
async def ensure(
|
|
401
|
+
self,
|
|
402
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
403
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
404
|
+
) -> ActionResult[list[Page[T, TParam]]]:
|
|
405
|
+
"""Ensure an initial fetch has started, then wait for completion."""
|
|
406
|
+
if len(self.pages) == 0 and self.status() == "loading":
|
|
407
|
+
if self._queue_task is None or self._queue_task.done():
|
|
408
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
409
|
+
self._enqueue(Refetch(fetch_fn=fn, observer=observer))
|
|
410
|
+
return await self.wait()
|
|
411
|
+
|
|
344
412
|
def observe(self, observer: Any):
|
|
345
413
|
self._observers.append(observer)
|
|
346
414
|
self.cancel_gc()
|
|
347
415
|
gc_time = getattr(observer, "_gc_time", 0)
|
|
348
416
|
if gc_time and gc_time > 0:
|
|
349
417
|
self.cfg.gc_time = max(self.cfg.gc_time, gc_time)
|
|
418
|
+
self._update_interval()
|
|
350
419
|
|
|
351
420
|
def unobserve(self, observer: "InfiniteQueryResult[T, TParam]"):
|
|
352
421
|
"""Unregister an observer. Cancels pending actions. Schedules GC if no observers remain."""
|
|
353
422
|
if observer in self._observers:
|
|
354
423
|
self._observers.remove(observer)
|
|
424
|
+
self._update_interval()
|
|
355
425
|
|
|
356
426
|
# Cancel pending actions from this observer
|
|
357
427
|
self._cancel_observer_actions(observer)
|
|
@@ -493,7 +563,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
493
563
|
if self._queue_task is None or self._queue_task.done():
|
|
494
564
|
# Create task with no reactive scope to avoid inheriting deps from caller
|
|
495
565
|
with Untrack():
|
|
496
|
-
self._queue_task =
|
|
566
|
+
self._queue_task = create_task(self._process_queue())
|
|
497
567
|
return self._queue_task
|
|
498
568
|
|
|
499
569
|
async def _process_queue(self):
|
|
@@ -630,11 +700,17 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
630
700
|
(i for i, p in enumerate(self.pages) if p.param == action.param),
|
|
631
701
|
None,
|
|
632
702
|
)
|
|
633
|
-
if idx is None:
|
|
634
|
-
return None
|
|
635
703
|
|
|
636
704
|
page = await action.fetch_fn(action.param)
|
|
637
|
-
|
|
705
|
+
|
|
706
|
+
if idx is None:
|
|
707
|
+
# Page doesn't exist - jump to this page, clearing existing pages
|
|
708
|
+
self.pages.clear()
|
|
709
|
+
self.pages.append(Page(page, action.param))
|
|
710
|
+
else:
|
|
711
|
+
# Page exists, update it
|
|
712
|
+
self.pages[idx] = Page(page, action.param)
|
|
713
|
+
|
|
638
714
|
await self.commit()
|
|
639
715
|
return page
|
|
640
716
|
|
|
@@ -708,7 +784,10 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
708
784
|
cancel_fetch: bool = False,
|
|
709
785
|
) -> ActionResult[T | None]:
|
|
710
786
|
"""
|
|
711
|
-
Refetch
|
|
787
|
+
Refetch a page by its param. Queued for sequential execution.
|
|
788
|
+
|
|
789
|
+
If the page doesn't exist, clears existing pages and loads the requested
|
|
790
|
+
page as the new starting point.
|
|
712
791
|
|
|
713
792
|
Note: Prefer calling refetch_page() on InfiniteQueryResult to ensure the
|
|
714
793
|
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
@@ -725,6 +804,9 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
725
804
|
self._cancel_queue()
|
|
726
805
|
if self._queue_task and not self._queue_task.done():
|
|
727
806
|
self._queue_task.cancel()
|
|
807
|
+
if self._interval_effect is not None:
|
|
808
|
+
self._interval_effect.dispose()
|
|
809
|
+
self._interval_effect = None
|
|
728
810
|
if self.cfg.on_dispose:
|
|
729
811
|
self.cfg.on_dispose(self)
|
|
730
812
|
|
|
@@ -778,8 +860,7 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
778
860
|
_on_success: Callable[[list[Page[T, TParam]]], Awaitable[None] | None] | None
|
|
779
861
|
_on_error: Callable[[Exception], Awaitable[None] | None] | None
|
|
780
862
|
_observe_effect: Effect
|
|
781
|
-
|
|
782
|
-
_data_computed: Computed[list[Page[T, TParam]] | None]
|
|
863
|
+
_data_computed: Computed[list[Page[T, TParam]] | None | Missing]
|
|
783
864
|
_enabled: Signal[bool]
|
|
784
865
|
_fetch_on_mount: bool
|
|
785
866
|
|
|
@@ -801,13 +882,17 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
801
882
|
self._fetch_fn = fetch_fn
|
|
802
883
|
self._stale_time = stale_time
|
|
803
884
|
self._gc_time = gc_time
|
|
804
|
-
|
|
885
|
+
interval = (
|
|
886
|
+
refetch_interval
|
|
887
|
+
if refetch_interval is not None and refetch_interval > 0
|
|
888
|
+
else None
|
|
889
|
+
)
|
|
890
|
+
self._refetch_interval = interval
|
|
805
891
|
self._keep_previous_data = keep_previous_data
|
|
806
892
|
self._on_success = on_success
|
|
807
893
|
self._on_error = on_error
|
|
808
894
|
self._enabled = Signal(enabled, name=f"inf_query.enabled({query().key})")
|
|
809
895
|
self._fetch_on_mount = fetch_on_mount
|
|
810
|
-
self._interval_effect = None
|
|
811
896
|
|
|
812
897
|
def observe_effect():
|
|
813
898
|
q = self._query()
|
|
@@ -816,8 +901,13 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
816
901
|
with Untrack():
|
|
817
902
|
q.observe(self)
|
|
818
903
|
|
|
819
|
-
if
|
|
820
|
-
|
|
904
|
+
# Skip if query interval is active - interval effect handles initial fetch
|
|
905
|
+
if enabled and fetch_on_mount and not q.has_interval:
|
|
906
|
+
# Fetch if no data loaded yet or if existing data is stale
|
|
907
|
+
if not q.is_fetching() and (
|
|
908
|
+
q.status() == "loading" or self.is_stale()
|
|
909
|
+
):
|
|
910
|
+
q.invalidate()
|
|
821
911
|
|
|
822
912
|
# Return cleanup function that captures the query (old query on key change)
|
|
823
913
|
def cleanup():
|
|
@@ -831,26 +921,9 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
831
921
|
immediate=True,
|
|
832
922
|
)
|
|
833
923
|
self._data_computed = Computed(
|
|
834
|
-
self._data_computed_fn,
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
# Set up interval effect if interval is specified
|
|
838
|
-
if refetch_interval is not None and refetch_interval > 0:
|
|
839
|
-
self._setup_interval_effect(refetch_interval)
|
|
840
|
-
|
|
841
|
-
def _setup_interval_effect(self, interval: float):
|
|
842
|
-
"""Create an effect that invalidates the query at the specified interval."""
|
|
843
|
-
|
|
844
|
-
def interval_fn():
|
|
845
|
-
# Read enabled to make this effect reactive to enabled changes
|
|
846
|
-
if self._enabled():
|
|
847
|
-
self._query().invalidate()
|
|
848
|
-
|
|
849
|
-
self._interval_effect = Effect(
|
|
850
|
-
interval_fn,
|
|
851
|
-
name=f"inf_query_interval({self._query().key})",
|
|
852
|
-
interval=interval,
|
|
853
|
-
immediate=True,
|
|
924
|
+
self._data_computed_fn,
|
|
925
|
+
name=f"inf_query_data({self._query().key})",
|
|
926
|
+
initial_value=MISSING,
|
|
854
927
|
)
|
|
855
928
|
|
|
856
929
|
@property
|
|
@@ -878,18 +951,19 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
878
951
|
return self._query().error.read()
|
|
879
952
|
|
|
880
953
|
def _data_computed_fn(
|
|
881
|
-
self, prev: list[Page[T, TParam]] | None
|
|
882
|
-
) -> list[Page[T, TParam]] | None:
|
|
954
|
+
self, prev: list[Page[T, TParam]] | None | Missing
|
|
955
|
+
) -> list[Page[T, TParam]] | None | Missing:
|
|
883
956
|
query = self._query()
|
|
884
|
-
if self._keep_previous_data and query.status()
|
|
957
|
+
if self._keep_previous_data and query.status() == "loading":
|
|
885
958
|
return prev
|
|
886
959
|
# Access pages.version to subscribe to structural changes
|
|
887
|
-
|
|
888
|
-
|
|
960
|
+
if len(query.pages) == 0:
|
|
961
|
+
return MISSING
|
|
962
|
+
return unwrap(query.pages)
|
|
889
963
|
|
|
890
964
|
@property
|
|
891
965
|
def data(self) -> list[Page[T, TParam]] | None:
|
|
892
|
-
return self._data_computed()
|
|
966
|
+
return none_if_missing(self._data_computed())
|
|
893
967
|
|
|
894
968
|
@property
|
|
895
969
|
def pages(self) -> list[T] | None:
|
|
@@ -918,8 +992,6 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
918
992
|
return isinstance(self._query().current_action(), FetchPrevious)
|
|
919
993
|
|
|
920
994
|
def is_stale(self) -> bool:
|
|
921
|
-
if self._stale_time <= 0:
|
|
922
|
-
return False
|
|
923
995
|
query = self._query()
|
|
924
996
|
return (time.time() - query.last_updated.read()) > self._stale_time
|
|
925
997
|
|
|
@@ -985,15 +1057,20 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
985
1057
|
async def wait(self) -> ActionResult[list[Page[T, TParam]]]:
|
|
986
1058
|
return await self._query().wait(fetch_fn=self._fetch_fn, observer=self)
|
|
987
1059
|
|
|
1060
|
+
async def ensure(self) -> ActionResult[list[Page[T, TParam]]]:
|
|
1061
|
+
return await self._query().ensure(fetch_fn=self._fetch_fn, observer=self)
|
|
1062
|
+
|
|
988
1063
|
def invalidate(self):
|
|
989
1064
|
query = self._query()
|
|
990
1065
|
query.invalidate(fetch_fn=self._fetch_fn, observer=self)
|
|
991
1066
|
|
|
992
1067
|
def enable(self):
|
|
993
1068
|
self._enabled.write(True)
|
|
1069
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
994
1070
|
|
|
995
1071
|
def disable(self):
|
|
996
1072
|
self._enabled.write(False)
|
|
1073
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
997
1074
|
|
|
998
1075
|
def set_error(self, error: Exception):
|
|
999
1076
|
query = self._query()
|
|
@@ -1002,8 +1079,6 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
1002
1079
|
@override
|
|
1003
1080
|
def dispose(self):
|
|
1004
1081
|
"""Clean up the result and its observe effect."""
|
|
1005
|
-
if self._interval_effect is not None:
|
|
1006
|
-
self._interval_effect.dispose()
|
|
1007
1082
|
self._observe_effect.dispose()
|
|
1008
1083
|
|
|
1009
1084
|
|
|
@@ -1023,6 +1098,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1023
1098
|
|
|
1024
1099
|
Optional decorators:
|
|
1025
1100
|
- ``@infinite_query_prop.get_previous_page_param``: For bi-directional pagination.
|
|
1101
|
+
- ``@infinite_query_prop.initial_data``: Provide initial pages.
|
|
1026
1102
|
- ``@infinite_query_prop.on_success``: Handle successful fetch.
|
|
1027
1103
|
- ``@infinite_query_prop.on_error``: Handle fetch errors.
|
|
1028
1104
|
|
|
@@ -1058,6 +1134,12 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1058
1134
|
_refetch_interval: float | None
|
|
1059
1135
|
_retries: int
|
|
1060
1136
|
_retry_delay: float
|
|
1137
|
+
_initial_data: (
|
|
1138
|
+
list[Page[T, TParam]]
|
|
1139
|
+
| Callable[[TState], list[Page[T, TParam]]]
|
|
1140
|
+
| Missing
|
|
1141
|
+
| None
|
|
1142
|
+
)
|
|
1061
1143
|
_initial_page_param: TParam
|
|
1062
1144
|
_get_next_page_param: (
|
|
1063
1145
|
Callable[[TState, list[Page[T, TParam]]], TParam | None] | None
|
|
@@ -1109,6 +1191,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1109
1191
|
self._retry_delay = retry_delay
|
|
1110
1192
|
self._on_success_fn = None
|
|
1111
1193
|
self._on_error_fn = None
|
|
1194
|
+
self._initial_data = MISSING
|
|
1112
1195
|
self._key = key
|
|
1113
1196
|
self._initial_data_updated_at = initial_data_updated_at
|
|
1114
1197
|
self._enabled = enabled
|
|
@@ -1139,6 +1222,16 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1139
1222
|
self._on_error_fn = fn # pyright: ignore[reportAttributeAccessIssue]
|
|
1140
1223
|
return fn
|
|
1141
1224
|
|
|
1225
|
+
def initial_data(
|
|
1226
|
+
self, fn: Callable[[TState], list[Page[T, TParam]]]
|
|
1227
|
+
) -> Callable[[TState], list[Page[T, TParam]]]:
|
|
1228
|
+
if self._initial_data is not MISSING:
|
|
1229
|
+
raise RuntimeError(
|
|
1230
|
+
f"Duplicate initial_data() decorator for infinite query '{self.name}'. Only one is allowed."
|
|
1231
|
+
)
|
|
1232
|
+
self._initial_data = fn
|
|
1233
|
+
return fn
|
|
1234
|
+
|
|
1142
1235
|
def get_next_page_param(
|
|
1143
1236
|
self,
|
|
1144
1237
|
fn: Callable[[TState, list[Page[T, TParam]]], TParam | None],
|
|
@@ -1187,8 +1280,23 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1187
1280
|
raise RuntimeError(
|
|
1188
1281
|
f"key is required for infinite query '{self.name}'. Provide a key via @infinite_query(key=...) or @{self.name}.key decorator."
|
|
1189
1282
|
)
|
|
1283
|
+
raw_initial = (
|
|
1284
|
+
call_flexible(self._initial_data, state)
|
|
1285
|
+
if callable(self._initial_data)
|
|
1286
|
+
else self._initial_data
|
|
1287
|
+
)
|
|
1288
|
+
initial_data = (
|
|
1289
|
+
MISSING
|
|
1290
|
+
if raw_initial is MISSING
|
|
1291
|
+
else cast(list[Page[T, TParam]] | None, raw_initial)
|
|
1292
|
+
)
|
|
1190
1293
|
query = self._resolve_keyed(
|
|
1191
|
-
state,
|
|
1294
|
+
state,
|
|
1295
|
+
fetch_fn,
|
|
1296
|
+
next_fn,
|
|
1297
|
+
prev_fn,
|
|
1298
|
+
initial_data,
|
|
1299
|
+
self._initial_data_updated_at,
|
|
1192
1300
|
)
|
|
1193
1301
|
|
|
1194
1302
|
on_success = None
|
|
@@ -1224,6 +1332,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1224
1332
|
fetch_fn: Callable[[TParam], Awaitable[T]],
|
|
1225
1333
|
next_fn: Callable[[list[Page[T, TParam]]], TParam | None],
|
|
1226
1334
|
prev_fn: Callable[[list[Page[T, TParam]]], TParam | None] | None,
|
|
1335
|
+
initial_data: list[Page[T, TParam]] | Missing | None,
|
|
1227
1336
|
initial_data_updated_at: float | dt.datetime | None,
|
|
1228
1337
|
) -> Computed[InfiniteQuery[T, TParam]]:
|
|
1229
1338
|
assert self._key is not None
|
|
@@ -1254,6 +1363,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1254
1363
|
get_next_page_param=next_fn,
|
|
1255
1364
|
get_previous_page_param=prev_fn,
|
|
1256
1365
|
max_pages=self._max_pages,
|
|
1366
|
+
initial_data=initial_data,
|
|
1257
1367
|
gc_time=self._gc_time,
|
|
1258
1368
|
retries=self._retries,
|
|
1259
1369
|
retry_delay=self._retry_delay,
|
|
@@ -1347,7 +1457,7 @@ def infinite_query(
|
|
|
1347
1457
|
stale_time: Seconds before data is considered stale (default 0.0).
|
|
1348
1458
|
gc_time: Seconds to keep unused query in cache (default 300.0).
|
|
1349
1459
|
refetch_interval: Auto-refetch interval in seconds (default None).
|
|
1350
|
-
keep_previous_data: Keep previous data while
|
|
1460
|
+
keep_previous_data: Keep previous data while loading (default False).
|
|
1351
1461
|
retries: Number of retry attempts on failure (default 3).
|
|
1352
1462
|
retry_delay: Delay between retries in seconds (default 2.0).
|
|
1353
1463
|
initial_data_updated_at: Timestamp for initial data staleness.
|
pulse/queries/protocol.py
CHANGED
|
@@ -90,6 +90,15 @@ class QueryResult(Protocol[T]):
|
|
|
90
90
|
"""
|
|
91
91
|
...
|
|
92
92
|
|
|
93
|
+
async def ensure(self) -> ActionResult[T]:
|
|
94
|
+
"""
|
|
95
|
+
Ensure an initial fetch has started, then wait for completion.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ActionResult containing either the data or an error.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
93
102
|
def invalidate(self) -> None:
|
|
94
103
|
"""Mark the query as stale and trigger a refetch if observed."""
|
|
95
104
|
...
|