pulse-framework 0.1.44__tar.gz → 0.1.46__tar.gz
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_framework-0.1.44 → pulse_framework-0.1.46}/PKG-INFO +1 -1
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/pyproject.toml +1 -1
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/__init__.py +1 -1
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/app.py +1 -1
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/client.py +7 -7
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/effect.py +16 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/infinite_query.py +138 -29
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/mutation.py +1 -15
- pulse_framework-0.1.46/src/pulse/queries/protocol.py +136 -0
- pulse_framework-0.1.46/src/pulse/queries/query.py +1213 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/store.py +11 -14
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/reactive.py +19 -1
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/render_session.py +41 -25
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/renderer.py +0 -43
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/types/event_handler.py +3 -2
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/vdom.py +100 -7
- pulse_framework-0.1.44/src/pulse/queries/query.py +0 -777
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/README.md +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/imports.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/css.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/hooks/states.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/elements.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/events.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/props.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/svg.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/tags.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/html/tags.pyi +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/queries/common.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.44 → pulse_framework-0.1.46}/src/pulse/version.py +0 -0
|
@@ -1357,7 +1357,7 @@ from pulse.queries.common import QueryKey as QueryKey
|
|
|
1357
1357
|
from pulse.queries.common import QueryStatus as QueryStatus
|
|
1358
1358
|
from pulse.queries.infinite_query import infinite_query as infinite_query
|
|
1359
1359
|
from pulse.queries.mutation import mutation as mutation
|
|
1360
|
-
from pulse.queries.
|
|
1360
|
+
from pulse.queries.protocol import QueryResult as QueryResult
|
|
1361
1361
|
from pulse.queries.query import query as query
|
|
1362
1362
|
|
|
1363
1363
|
# React component registry
|
|
@@ -675,7 +675,7 @@ class App:
|
|
|
675
675
|
if rid is not None:
|
|
676
676
|
render = self.render_sessions.get(rid)
|
|
677
677
|
if render:
|
|
678
|
-
render.
|
|
678
|
+
render.disconnect()
|
|
679
679
|
# Schedule cleanup after timeout (will keep session alive for reuse)
|
|
680
680
|
self._schedule_render_cleanup(rid)
|
|
681
681
|
|
|
@@ -5,7 +5,7 @@ from typing import Any, TypeVar, overload
|
|
|
5
5
|
from pulse.context import PulseContext
|
|
6
6
|
from pulse.queries.common import ActionResult, QueryKey
|
|
7
7
|
from pulse.queries.infinite_query import InfiniteQuery, Page
|
|
8
|
-
from pulse.queries.query import
|
|
8
|
+
from pulse.queries.query import KeyedQuery
|
|
9
9
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
@@ -62,11 +62,11 @@ class QueryClient:
|
|
|
62
62
|
# Query accessors
|
|
63
63
|
# ─────────────────────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
-
def get(self, key: QueryKey)
|
|
65
|
+
def get(self, key: QueryKey):
|
|
66
66
|
"""Get an existing regular query by key, or None if not found."""
|
|
67
67
|
return self._get_store().get(key)
|
|
68
68
|
|
|
69
|
-
def get_infinite(self, key: QueryKey)
|
|
69
|
+
def get_infinite(self, key: QueryKey):
|
|
70
70
|
"""Get an existing infinite query by key, or None if not found."""
|
|
71
71
|
return self._get_store().get_infinite(key)
|
|
72
72
|
|
|
@@ -75,7 +75,7 @@ class QueryClient:
|
|
|
75
75
|
filter: QueryFilter | None = None,
|
|
76
76
|
*,
|
|
77
77
|
include_infinite: bool = True,
|
|
78
|
-
) -> list[
|
|
78
|
+
) -> list[KeyedQuery[Any] | InfiniteQuery[Any, Any]]:
|
|
79
79
|
"""
|
|
80
80
|
Get all queries matching the filter.
|
|
81
81
|
|
|
@@ -89,7 +89,7 @@ class QueryClient:
|
|
|
89
89
|
"""
|
|
90
90
|
store = self._get_store()
|
|
91
91
|
predicate = _normalize_filter(filter)
|
|
92
|
-
results: list[
|
|
92
|
+
results: list[KeyedQuery[Any] | InfiniteQuery[Any, Any]] = []
|
|
93
93
|
|
|
94
94
|
for key, entry in store.items():
|
|
95
95
|
if predicate is not None and not predicate(key):
|
|
@@ -100,11 +100,11 @@ class QueryClient:
|
|
|
100
100
|
|
|
101
101
|
return results
|
|
102
102
|
|
|
103
|
-
def get_queries(self, filter: QueryFilter | None = None) -> list[
|
|
103
|
+
def get_queries(self, filter: QueryFilter | None = None) -> list[KeyedQuery[Any]]:
|
|
104
104
|
"""Get all regular queries matching the filter."""
|
|
105
105
|
store = self._get_store()
|
|
106
106
|
predicate = _normalize_filter(filter)
|
|
107
|
-
results: list[
|
|
107
|
+
results: list[KeyedQuery[Any]] = []
|
|
108
108
|
|
|
109
109
|
for key, entry in store.items():
|
|
110
110
|
if isinstance(entry, InfiniteQuery):
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
|
3
3
|
from typing import (
|
|
4
4
|
Any,
|
|
5
|
+
Literal,
|
|
5
6
|
Protocol,
|
|
6
7
|
override,
|
|
7
8
|
)
|
|
@@ -11,15 +12,21 @@ from pulse.reactive import AsyncEffect, Computed, Signal
|
|
|
11
12
|
|
|
12
13
|
class Fetcher(Protocol):
|
|
13
14
|
is_fetching: Signal[bool]
|
|
15
|
+
data: Signal[Any]
|
|
16
|
+
status: Signal[Literal["loading", "success", "error"]]
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class AsyncQueryEffect(AsyncEffect):
|
|
17
20
|
"""
|
|
18
21
|
Specialized AsyncEffect for queries that synchronously sets loading state
|
|
19
22
|
when rescheduled/run.
|
|
23
|
+
|
|
24
|
+
For unkeyed queries (deps=None), also resets data/status when re-running
|
|
25
|
+
due to dependency changes, to behave like keyed queries on key change.
|
|
20
26
|
"""
|
|
21
27
|
|
|
22
28
|
fetcher: Fetcher
|
|
29
|
+
_is_unkeyed: bool
|
|
23
30
|
|
|
24
31
|
def __init__(
|
|
25
32
|
self,
|
|
@@ -30,10 +37,19 @@ class AsyncQueryEffect(AsyncEffect):
|
|
|
30
37
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
31
38
|
):
|
|
32
39
|
self.fetcher = fetcher
|
|
40
|
+
# Unkeyed queries have deps=None (auto-track), keyed have deps=[] (no auto-track)
|
|
41
|
+
self._is_unkeyed = deps is None
|
|
33
42
|
super().__init__(fn, name=name, lazy=lazy, deps=deps)
|
|
34
43
|
|
|
35
44
|
@override
|
|
36
45
|
def run(self) -> asyncio.Task[Any]:
|
|
37
46
|
# Immediately set loading state before running the effect
|
|
38
47
|
self.fetcher.is_fetching.write(True)
|
|
48
|
+
|
|
49
|
+
# For unkeyed queries on re-run (dependency changed), reset data/status
|
|
50
|
+
# to behave like keyed queries when key changes (new Query with data=None)
|
|
51
|
+
if self._is_unkeyed and self.runs > 0:
|
|
52
|
+
self.fetcher.data.write(None)
|
|
53
|
+
self.fetcher.status.write("loading")
|
|
54
|
+
|
|
39
55
|
return super().run()
|
|
@@ -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,
|
|
@@ -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)
|