pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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 +10 -24
- pulse/app.py +3 -25
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +40 -8
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/queries/client.py +7 -7
- pulse/queries/effect.py +16 -0
- pulse/queries/infinite_query.py +138 -29
- pulse/queries/mutation.py +1 -15
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +610 -174
- pulse/queries/store.py +11 -14
- pulse/react_component.py +167 -14
- pulse/reactive.py +19 -1
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +185 -59
- pulse/renderer.py +80 -158
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/types/event_handler.py +3 -2
- pulse/vdom.py +212 -13
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
- pulse_framework-0.1.47.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.44.dist-info/RECORD +0 -79
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
pulse/queries/infinite_query.py
CHANGED
|
@@ -57,6 +57,8 @@ class Page(NamedTuple, Generic[T, TParam]):
|
|
|
57
57
|
class FetchNext(Generic[T, TParam]):
|
|
58
58
|
"""Fetch the next page."""
|
|
59
59
|
|
|
60
|
+
fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
61
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None
|
|
60
62
|
future: "asyncio.Future[ActionResult[Page[T, TParam] | None]]" = field(
|
|
61
63
|
default_factory=asyncio.Future
|
|
62
64
|
)
|
|
@@ -66,6 +68,8 @@ class FetchNext(Generic[T, TParam]):
|
|
|
66
68
|
class FetchPrevious(Generic[T, TParam]):
|
|
67
69
|
"""Fetch the previous page."""
|
|
68
70
|
|
|
71
|
+
fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
72
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None
|
|
69
73
|
future: "asyncio.Future[ActionResult[Page[T, TParam] | None]]" = field(
|
|
70
74
|
default_factory=asyncio.Future
|
|
71
75
|
)
|
|
@@ -75,6 +79,8 @@ class FetchPrevious(Generic[T, TParam]):
|
|
|
75
79
|
class Refetch(Generic[T, TParam]):
|
|
76
80
|
"""Refetch all pages."""
|
|
77
81
|
|
|
82
|
+
fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
83
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None
|
|
78
84
|
refetch_page: Callable[[T, int, list[T]], bool] | None = None
|
|
79
85
|
future: "asyncio.Future[ActionResult[list[Page[T, TParam]]]]" = field(
|
|
80
86
|
default_factory=asyncio.Future
|
|
@@ -85,7 +91,9 @@ class Refetch(Generic[T, TParam]):
|
|
|
85
91
|
class RefetchPage(Generic[T, TParam]):
|
|
86
92
|
"""Refetch a single page by param."""
|
|
87
93
|
|
|
94
|
+
fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
88
95
|
param: TParam
|
|
96
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None
|
|
89
97
|
future: "asyncio.Future[ActionResult[T | None]]" = field(
|
|
90
98
|
default_factory=asyncio.Future
|
|
91
99
|
)
|
|
@@ -113,9 +121,17 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
113
121
|
"""Paginated query that stores data as a list of Page(data, param)."""
|
|
114
122
|
|
|
115
123
|
key: QueryKey
|
|
116
|
-
fn: Callable[[TParam], Awaitable[T]]
|
|
117
124
|
cfg: InfiniteQueryConfig[T, TParam]
|
|
118
125
|
|
|
126
|
+
@property
|
|
127
|
+
def fn(self) -> Callable[[TParam], Awaitable[T]]:
|
|
128
|
+
"""Get the fetch function from the first observer."""
|
|
129
|
+
if len(self._observers) == 0:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
f"InfiniteQuery '{self.key}' has no observers. Cannot access fetch function."
|
|
132
|
+
)
|
|
133
|
+
return self._observers[0]._fetch_fn # pyright: ignore[reportPrivateUsage]
|
|
134
|
+
|
|
119
135
|
# Reactive state
|
|
120
136
|
pages: ReactiveList[Page[T, TParam]]
|
|
121
137
|
error: Signal[Exception | None]
|
|
@@ -139,7 +155,6 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
139
155
|
def __init__(
|
|
140
156
|
self,
|
|
141
157
|
key: QueryKey,
|
|
142
|
-
fn: Callable[[TParam], Awaitable[T]],
|
|
143
158
|
*,
|
|
144
159
|
initial_page_param: TParam,
|
|
145
160
|
get_next_page_param: Callable[[list[Page[T, TParam]]], TParam | None],
|
|
@@ -155,7 +170,6 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
155
170
|
on_dispose: Callable[[Any], None] | None = None,
|
|
156
171
|
):
|
|
157
172
|
self.key = key
|
|
158
|
-
self.fn = fn
|
|
159
173
|
|
|
160
174
|
self.cfg = InfiniteQueryConfig(
|
|
161
175
|
retries=retries,
|
|
@@ -287,12 +301,18 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
287
301
|
if updated_at is not None:
|
|
288
302
|
self.set_updated_at(updated_at)
|
|
289
303
|
|
|
290
|
-
async def wait(
|
|
304
|
+
async def wait(
|
|
305
|
+
self,
|
|
306
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
307
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
308
|
+
) -> ActionResult[list[Page[T, TParam]]]:
|
|
291
309
|
"""Wait for initial data or until queue is empty."""
|
|
292
310
|
# If no data and loading, enqueue initial fetch (unless already processing)
|
|
293
311
|
if len(self.pages) == 0 and self.status() == "loading":
|
|
294
312
|
if self._queue_task is None or self._queue_task.done():
|
|
295
|
-
|
|
313
|
+
# Use provided fetch_fn or fall back to first observer's fetch_fn
|
|
314
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
315
|
+
self._enqueue(Refetch(fetch_fn=fn, observer=observer))
|
|
296
316
|
# Wait for any in-progress queue processing
|
|
297
317
|
if self._queue_task and not self._queue_task.done():
|
|
298
318
|
await self._queue_task
|
|
@@ -308,9 +328,14 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
308
328
|
if gc_time and gc_time > 0:
|
|
309
329
|
self.cfg.gc_time = max(self.cfg.gc_time, gc_time)
|
|
310
330
|
|
|
311
|
-
def unobserve(self, observer:
|
|
331
|
+
def unobserve(self, observer: "InfiniteQueryResult[T, TParam]"):
|
|
332
|
+
"""Unregister an observer. Cancels pending actions. Schedules GC if no observers remain."""
|
|
312
333
|
if observer in self._observers:
|
|
313
334
|
self._observers.remove(observer)
|
|
335
|
+
|
|
336
|
+
# Cancel pending actions from this observer
|
|
337
|
+
self._cancel_observer_actions(observer)
|
|
338
|
+
|
|
314
339
|
if len(self._observers) == 0:
|
|
315
340
|
self.schedule_gc()
|
|
316
341
|
|
|
@@ -319,12 +344,18 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
319
344
|
*,
|
|
320
345
|
cancel_fetch: bool = False,
|
|
321
346
|
refetch_page: Callable[[T, int, list[T]], bool] | None = None,
|
|
347
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
348
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
322
349
|
):
|
|
323
350
|
"""Enqueue a refetch. Synchronous - does not wait for completion."""
|
|
324
351
|
if cancel_fetch:
|
|
325
352
|
self._cancel_queue()
|
|
326
353
|
if len(self._observers) > 0:
|
|
327
|
-
|
|
354
|
+
# Use provided fetch_fn or fall back to first observer's fetch_fn
|
|
355
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
356
|
+
self._enqueue(
|
|
357
|
+
Refetch(fetch_fn=fn, observer=observer, refetch_page=refetch_page)
|
|
358
|
+
)
|
|
328
359
|
|
|
329
360
|
def schedule_gc(self):
|
|
330
361
|
self.cancel_gc()
|
|
@@ -403,6 +434,26 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
403
434
|
self._queue_task.cancel()
|
|
404
435
|
self._queue_task = None
|
|
405
436
|
|
|
437
|
+
def _cancel_observer_actions(
|
|
438
|
+
self, observer: "InfiniteQueryResult[T, TParam]"
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Cancel pending actions from a specific observer.
|
|
441
|
+
|
|
442
|
+
Note: Does not cancel the currently executing action to avoid disrupting the
|
|
443
|
+
queue processor. The fetch will complete but results will be ignored since
|
|
444
|
+
the observer is disposed.
|
|
445
|
+
"""
|
|
446
|
+
# Cancel pending actions from this observer (not the currently executing one)
|
|
447
|
+
remaining: deque[Action[T, TParam]] = deque()
|
|
448
|
+
while self._queue:
|
|
449
|
+
action = self._queue.popleft()
|
|
450
|
+
if action.observer is observer:
|
|
451
|
+
if not action.future.done():
|
|
452
|
+
action.future.cancel()
|
|
453
|
+
else:
|
|
454
|
+
remaining.append(action)
|
|
455
|
+
self._queue = remaining
|
|
456
|
+
|
|
406
457
|
def _enqueue(
|
|
407
458
|
self,
|
|
408
459
|
action: "FetchNext[T, TParam] | FetchPrevious[T, TParam] | Refetch[T, TParam] | RefetchPage[T, TParam]",
|
|
@@ -493,7 +544,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
493
544
|
self.has_next_page.write(False)
|
|
494
545
|
return None
|
|
495
546
|
|
|
496
|
-
page = await
|
|
547
|
+
page = await action.fetch_fn(next_param)
|
|
497
548
|
page = Page(page, next_param)
|
|
498
549
|
self.pages.append(page)
|
|
499
550
|
self._trim_front()
|
|
@@ -508,7 +559,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
508
559
|
self.has_previous_page.write(False)
|
|
509
560
|
return None
|
|
510
561
|
|
|
511
|
-
data = await
|
|
562
|
+
data = await action.fetch_fn(prev_param)
|
|
512
563
|
page = Page(data, prev_param)
|
|
513
564
|
self.pages.insert(0, page)
|
|
514
565
|
self._trim_back()
|
|
@@ -519,7 +570,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
519
570
|
self, action: "Refetch[T, TParam]"
|
|
520
571
|
) -> list[Page[T, TParam]]:
|
|
521
572
|
if len(self.pages) == 0:
|
|
522
|
-
page = await
|
|
573
|
+
page = await action.fetch_fn(self.cfg.initial_page_param)
|
|
523
574
|
self.pages.append(Page(page, self.cfg.initial_page_param))
|
|
524
575
|
await self.commit()
|
|
525
576
|
return self.pages
|
|
@@ -538,7 +589,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
538
589
|
)
|
|
539
590
|
|
|
540
591
|
if should_refetch:
|
|
541
|
-
page = await
|
|
592
|
+
page = await action.fetch_fn(page_param)
|
|
542
593
|
else:
|
|
543
594
|
page = old_page.data
|
|
544
595
|
self.pages[idx] = Page(page, page_param)
|
|
@@ -562,7 +613,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
562
613
|
if idx is None:
|
|
563
614
|
return None
|
|
564
615
|
|
|
565
|
-
page = await
|
|
616
|
+
page = await action.fetch_fn(action.param)
|
|
566
617
|
self.pages[idx] = Page(page, action.param)
|
|
567
618
|
await self.commit()
|
|
568
619
|
return page
|
|
@@ -573,40 +624,80 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
573
624
|
|
|
574
625
|
async def fetch_next_page(
|
|
575
626
|
self,
|
|
627
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
576
628
|
*,
|
|
629
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
577
630
|
cancel_fetch: bool = False,
|
|
578
631
|
) -> ActionResult[Page[T, TParam] | None]:
|
|
579
|
-
"""
|
|
580
|
-
|
|
632
|
+
"""
|
|
633
|
+
Fetch the next page. Queued for sequential execution.
|
|
634
|
+
|
|
635
|
+
Note: Prefer calling fetch_next_page() on InfiniteQueryResult to ensure the
|
|
636
|
+
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
637
|
+
the first observer's fetch function if not provided.
|
|
638
|
+
"""
|
|
639
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
640
|
+
action: FetchNext[T, TParam] = FetchNext(fetch_fn=fn, observer=observer)
|
|
581
641
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
582
642
|
|
|
583
643
|
async def fetch_previous_page(
|
|
584
644
|
self,
|
|
645
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
585
646
|
*,
|
|
647
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
586
648
|
cancel_fetch: bool = False,
|
|
587
649
|
) -> ActionResult[Page[T, TParam] | None]:
|
|
588
|
-
"""
|
|
589
|
-
|
|
650
|
+
"""
|
|
651
|
+
Fetch the previous page. Queued for sequential execution.
|
|
652
|
+
|
|
653
|
+
Note: Prefer calling fetch_previous_page() on InfiniteQueryResult to ensure
|
|
654
|
+
the correct fetch function is used. When called directly on InfiniteQuery,
|
|
655
|
+
uses the first observer's fetch function if not provided.
|
|
656
|
+
"""
|
|
657
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
658
|
+
action: FetchPrevious[T, TParam] = FetchPrevious(fetch_fn=fn, observer=observer)
|
|
590
659
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
591
660
|
|
|
592
661
|
async def refetch(
|
|
593
662
|
self,
|
|
663
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
594
664
|
*,
|
|
665
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
595
666
|
cancel_fetch: bool = False,
|
|
596
667
|
refetch_page: Callable[[T, int, list[T]], bool] | None = None,
|
|
597
668
|
) -> ActionResult[list[Page[T, TParam]]]:
|
|
598
|
-
"""
|
|
599
|
-
|
|
669
|
+
"""
|
|
670
|
+
Refetch all pages. Queued for sequential execution.
|
|
671
|
+
|
|
672
|
+
Note: Prefer calling refetch() on InfiniteQueryResult to ensure the correct
|
|
673
|
+
fetch function is used. When called directly on InfiniteQuery, uses the first
|
|
674
|
+
observer's fetch function if not provided.
|
|
675
|
+
"""
|
|
676
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
677
|
+
action: Refetch[T, TParam] = Refetch(
|
|
678
|
+
fetch_fn=fn, observer=observer, refetch_page=refetch_page
|
|
679
|
+
)
|
|
600
680
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
601
681
|
|
|
602
682
|
async def refetch_page(
|
|
603
683
|
self,
|
|
604
684
|
param: TParam,
|
|
685
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
605
686
|
*,
|
|
687
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
606
688
|
cancel_fetch: bool = False,
|
|
607
689
|
) -> ActionResult[T | None]:
|
|
608
|
-
"""
|
|
609
|
-
|
|
690
|
+
"""
|
|
691
|
+
Refetch an existing page by its param. Queued for sequential execution.
|
|
692
|
+
|
|
693
|
+
Note: Prefer calling refetch_page() on InfiniteQueryResult to ensure the
|
|
694
|
+
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
695
|
+
the first observer's fetch function if not provided.
|
|
696
|
+
"""
|
|
697
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
698
|
+
action: RefetchPage[T, TParam] = RefetchPage(
|
|
699
|
+
fetch_fn=fn, param=param, observer=observer
|
|
700
|
+
)
|
|
610
701
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
611
702
|
|
|
612
703
|
@override
|
|
@@ -628,6 +719,7 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
628
719
|
"""
|
|
629
720
|
|
|
630
721
|
_query: Computed[InfiniteQuery[T, TParam]]
|
|
722
|
+
_fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
631
723
|
_stale_time: float
|
|
632
724
|
_gc_time: float
|
|
633
725
|
_refetch_interval: float | None
|
|
@@ -643,6 +735,7 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
643
735
|
def __init__(
|
|
644
736
|
self,
|
|
645
737
|
query: Computed[InfiniteQuery[T, TParam]],
|
|
738
|
+
fetch_fn: Callable[[TParam], Awaitable[T]],
|
|
646
739
|
stale_time: float = 0.0,
|
|
647
740
|
gc_time: float = 300.0,
|
|
648
741
|
refetch_interval: float | None = None,
|
|
@@ -654,6 +747,7 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
654
747
|
fetch_on_mount: bool = True,
|
|
655
748
|
):
|
|
656
749
|
self._query = query
|
|
750
|
+
self._fetch_fn = fetch_fn
|
|
657
751
|
self._stale_time = stale_time
|
|
658
752
|
self._gc_time = gc_time
|
|
659
753
|
self._refetch_interval = refetch_interval
|
|
@@ -667,12 +761,14 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
667
761
|
def observe_effect():
|
|
668
762
|
q = self._query()
|
|
669
763
|
enabled = self._enabled()
|
|
764
|
+
|
|
670
765
|
with Untrack():
|
|
671
766
|
q.observe(self)
|
|
672
767
|
|
|
673
|
-
|
|
674
|
-
|
|
768
|
+
if enabled and fetch_on_mount and self.is_stale():
|
|
769
|
+
q.invalidate()
|
|
675
770
|
|
|
771
|
+
# Return cleanup function that captures the query (old query on key change)
|
|
676
772
|
def cleanup():
|
|
677
773
|
q.unobserve(self)
|
|
678
774
|
|
|
@@ -781,14 +877,18 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
781
877
|
*,
|
|
782
878
|
cancel_fetch: bool = False,
|
|
783
879
|
) -> ActionResult[Page[T, TParam] | None]:
|
|
784
|
-
return await self._query().fetch_next_page(
|
|
880
|
+
return await self._query().fetch_next_page(
|
|
881
|
+
self._fetch_fn, observer=self, cancel_fetch=cancel_fetch
|
|
882
|
+
)
|
|
785
883
|
|
|
786
884
|
async def fetch_previous_page(
|
|
787
885
|
self,
|
|
788
886
|
*,
|
|
789
887
|
cancel_fetch: bool = False,
|
|
790
888
|
) -> ActionResult[Page[T, TParam] | None]:
|
|
791
|
-
return await self._query().fetch_previous_page(
|
|
889
|
+
return await self._query().fetch_previous_page(
|
|
890
|
+
self._fetch_fn, observer=self, cancel_fetch=cancel_fetch
|
|
891
|
+
)
|
|
792
892
|
|
|
793
893
|
async def fetch_page(
|
|
794
894
|
self,
|
|
@@ -796,7 +896,12 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
796
896
|
*,
|
|
797
897
|
cancel_fetch: bool = False,
|
|
798
898
|
) -> ActionResult[T | None]:
|
|
799
|
-
return await self._query().refetch_page(
|
|
899
|
+
return await self._query().refetch_page(
|
|
900
|
+
page_param,
|
|
901
|
+
fetch_fn=self._fetch_fn,
|
|
902
|
+
observer=self,
|
|
903
|
+
cancel_fetch=cancel_fetch,
|
|
904
|
+
)
|
|
800
905
|
|
|
801
906
|
def set_initial_data(
|
|
802
907
|
self,
|
|
@@ -820,15 +925,18 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
820
925
|
refetch_page: Callable[[T, int, list[T]], bool] | None = None,
|
|
821
926
|
) -> ActionResult[list[Page[T, TParam]]]:
|
|
822
927
|
return await self._query().refetch(
|
|
823
|
-
|
|
928
|
+
self._fetch_fn,
|
|
929
|
+
observer=self,
|
|
930
|
+
cancel_fetch=cancel_fetch,
|
|
931
|
+
refetch_page=refetch_page,
|
|
824
932
|
)
|
|
825
933
|
|
|
826
934
|
async def wait(self) -> ActionResult[list[Page[T, TParam]]]:
|
|
827
|
-
return await self._query().wait()
|
|
935
|
+
return await self._query().wait(fetch_fn=self._fetch_fn, observer=self)
|
|
828
936
|
|
|
829
937
|
def invalidate(self):
|
|
830
938
|
query = self._query()
|
|
831
|
-
query.invalidate()
|
|
939
|
+
query.invalidate(fetch_fn=self._fetch_fn, observer=self)
|
|
832
940
|
|
|
833
941
|
def enable(self):
|
|
834
942
|
self._enabled.write(True)
|
|
@@ -842,6 +950,7 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
842
950
|
|
|
843
951
|
@override
|
|
844
952
|
def dispose(self):
|
|
953
|
+
"""Clean up the result and its observe effect."""
|
|
845
954
|
if self._interval_effect is not None:
|
|
846
955
|
self._interval_effect.dispose()
|
|
847
956
|
self._observe_effect.dispose()
|
|
@@ -1001,6 +1110,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1001
1110
|
|
|
1002
1111
|
result = InfiniteQueryResult(
|
|
1003
1112
|
query=query,
|
|
1113
|
+
fetch_fn=fetch_fn,
|
|
1004
1114
|
stale_time=self._stale_time,
|
|
1005
1115
|
keep_previous_data=self._keep_previous_data,
|
|
1006
1116
|
gc_time=self._gc_time,
|
|
@@ -1048,7 +1158,6 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1048
1158
|
InfiniteQuery[T, TParam],
|
|
1049
1159
|
store.ensure_infinite(
|
|
1050
1160
|
key,
|
|
1051
|
-
fetch_fn,
|
|
1052
1161
|
initial_page_param=self._initial_page_param,
|
|
1053
1162
|
get_next_page_param=next_fn,
|
|
1054
1163
|
get_previous_page_param=prev_fn,
|
pulse/queries/mutation.py
CHANGED
|
@@ -147,18 +147,12 @@ class MutationProperty(Generic[T, TState, P], InitializableProperty):
|
|
|
147
147
|
@overload
|
|
148
148
|
def mutation(
|
|
149
149
|
fn: Callable[Concatenate[TState, P], Awaitable[T]],
|
|
150
|
-
*,
|
|
151
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
152
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
153
150
|
) -> MutationProperty[T, TState, P]: ...
|
|
154
151
|
|
|
155
152
|
|
|
156
153
|
@overload
|
|
157
154
|
def mutation(
|
|
158
155
|
fn: None = None,
|
|
159
|
-
*,
|
|
160
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
161
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
162
156
|
) -> Callable[
|
|
163
157
|
[Callable[Concatenate[TState, P], Awaitable[T]]], MutationProperty[T, TState, P]
|
|
164
158
|
]: ...
|
|
@@ -166,9 +160,6 @@ def mutation(
|
|
|
166
160
|
|
|
167
161
|
def mutation(
|
|
168
162
|
fn: Callable[Concatenate[TState, P], Awaitable[T]] | None = None,
|
|
169
|
-
*,
|
|
170
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
171
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
172
163
|
):
|
|
173
164
|
def decorator(func: Callable[Concatenate[TState, P], Awaitable[T]], /):
|
|
174
165
|
sig = inspect.signature(func)
|
|
@@ -177,12 +168,7 @@ def mutation(
|
|
|
177
168
|
if len(params) == 0 or params[0].name != "self":
|
|
178
169
|
raise TypeError("@mutation method must have 'self' as first argument")
|
|
179
170
|
|
|
180
|
-
return MutationProperty(
|
|
181
|
-
func.__name__,
|
|
182
|
-
func,
|
|
183
|
-
on_success=on_success,
|
|
184
|
-
on_error=on_error,
|
|
185
|
-
)
|
|
171
|
+
return MutationProperty(func.__name__, func)
|
|
186
172
|
|
|
187
173
|
if fn:
|
|
188
174
|
return decorator(fn)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Protocol, TypeVar, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from pulse.queries.common import ActionResult, QueryStatus
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class QueryResult(Protocol[T]):
|
|
12
|
+
"""
|
|
13
|
+
Unified query result interface for both keyed and unkeyed queries.
|
|
14
|
+
|
|
15
|
+
This protocol defines the public API that all query results expose,
|
|
16
|
+
regardless of whether they use keyed (cached/shared) or unkeyed
|
|
17
|
+
(dependency-tracked) execution strategies.
|
|
18
|
+
|
|
19
|
+
Keyed queries use a session-wide cache and explicit key functions to
|
|
20
|
+
determine when to refetch. Unkeyed queries automatically track reactive
|
|
21
|
+
dependencies and refetch when those dependencies change.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Status properties
|
|
25
|
+
@property
|
|
26
|
+
def status(self) -> QueryStatus:
|
|
27
|
+
"""Current query status: 'loading', 'success', or 'error'."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_loading(self) -> bool:
|
|
32
|
+
"""True if the query has not yet completed its initial fetch."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_success(self) -> bool:
|
|
37
|
+
"""True if the query completed successfully."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_error(self) -> bool:
|
|
42
|
+
"""True if the query completed with an error."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_fetching(self) -> bool:
|
|
47
|
+
"""True if a fetch is currently in progress (including refetches)."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_scheduled(self) -> bool:
|
|
52
|
+
"""True if a fetch is scheduled or currently running."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
# Data properties
|
|
56
|
+
@property
|
|
57
|
+
def data(self) -> T | None:
|
|
58
|
+
"""The query result data, or None if not yet available."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def error(self) -> Exception | None:
|
|
63
|
+
"""The error from the last fetch, or None if no error."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
# Query operations
|
|
67
|
+
def is_stale(self) -> bool:
|
|
68
|
+
"""Check if the query data is stale based on stale_time."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
async def refetch(self, cancel_refetch: bool = True) -> ActionResult[T]:
|
|
72
|
+
"""
|
|
73
|
+
Refetch the query data.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
cancel_refetch: If True (default), cancels any in-flight request
|
|
77
|
+
before starting a new one. If False, deduplicates requests.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ActionResult containing either the data or an error.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
async def wait(self) -> ActionResult[T]:
|
|
85
|
+
"""
|
|
86
|
+
Wait for the current fetch to complete.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
ActionResult containing either the data or an error.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def invalidate(self) -> None:
|
|
94
|
+
"""Mark the query as stale and trigger a refetch if observed."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
# Data manipulation
|
|
98
|
+
def set_data(self, data: T | Callable[[T | None], T]) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Optimistically set data without changing loading/error state.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
data: The new data value, or a function that receives the current
|
|
104
|
+
data and returns the new data.
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
def set_initial_data(
|
|
109
|
+
self,
|
|
110
|
+
data: T | Callable[[], T],
|
|
111
|
+
*,
|
|
112
|
+
updated_at: float | dt.datetime | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Set data as if it were provided as initial_data.
|
|
116
|
+
|
|
117
|
+
Only takes effect if the query is still in 'loading' state.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
data: The initial data value, or a function that returns it.
|
|
121
|
+
updated_at: Optional timestamp to seed staleness calculations.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def set_error(self, error: Exception) -> None:
|
|
126
|
+
"""Set error state on the query."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
# Enable/disable
|
|
130
|
+
def enable(self) -> None:
|
|
131
|
+
"""Enable the query, allowing it to fetch."""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def disable(self) -> None:
|
|
135
|
+
"""Disable the query, preventing it from fetching."""
|
|
136
|
+
...
|