pulse-framework 0.1.63__tar.gz → 0.1.64__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.63 → pulse_framework-0.1.64}/PKG-INFO +1 -1
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/pyproject.toml +1 -1
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/infinite_query.py +110 -37
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/protocol.py +9 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/query.py +106 -37
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/README.md +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/app.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/code_analysis.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/component.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/hooks/state.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/obj.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/pulse.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/react.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/client.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/common.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/mutation.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/queries/store.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/render_session.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/requirements.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/test_helpers.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/assets.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/dynamic_import.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/emit_context.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/function.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/imports.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/js_module.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/nodes.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/py_module.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/transpiler.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/vdom.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/version.py +0 -0
|
@@ -152,6 +152,61 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
152
152
|
)
|
|
153
153
|
return self._observers[0]._fetch_fn # pyright: ignore[reportPrivateUsage]
|
|
154
154
|
|
|
155
|
+
@property
|
|
156
|
+
def has_interval(self) -> bool:
|
|
157
|
+
return self._interval is not None
|
|
158
|
+
|
|
159
|
+
def _select_interval_observer(
|
|
160
|
+
self,
|
|
161
|
+
) -> tuple[float | None, "InfiniteQueryResult[T, TParam] | None"]:
|
|
162
|
+
min_interval: float | None = None
|
|
163
|
+
selected: "InfiniteQueryResult[T, TParam] | None" = None
|
|
164
|
+
|
|
165
|
+
for obs in reversed(self._observers):
|
|
166
|
+
interval = obs._refetch_interval # pyright: ignore[reportPrivateUsage]
|
|
167
|
+
if interval is None:
|
|
168
|
+
continue
|
|
169
|
+
if not obs._enabled.value: # pyright: ignore[reportPrivateUsage]
|
|
170
|
+
continue
|
|
171
|
+
if min_interval is None or interval < min_interval:
|
|
172
|
+
min_interval = interval
|
|
173
|
+
selected = obs
|
|
174
|
+
|
|
175
|
+
return min_interval, selected
|
|
176
|
+
|
|
177
|
+
def _create_interval_effect(self, interval: float) -> Effect:
|
|
178
|
+
def interval_fn():
|
|
179
|
+
observer = self._interval_observer
|
|
180
|
+
if observer is None:
|
|
181
|
+
return
|
|
182
|
+
self.invalidate(fetch_fn=observer._fetch_fn, observer=observer) # pyright: ignore[reportPrivateUsage]
|
|
183
|
+
|
|
184
|
+
return Effect(
|
|
185
|
+
interval_fn,
|
|
186
|
+
name=f"inf_query_interval({self.key})",
|
|
187
|
+
interval=interval,
|
|
188
|
+
immediate=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _update_interval(self) -> None:
|
|
192
|
+
new_interval, new_observer = self._select_interval_observer()
|
|
193
|
+
interval_changed = new_interval != self._interval
|
|
194
|
+
|
|
195
|
+
self._interval = new_interval
|
|
196
|
+
self._interval_observer = new_observer
|
|
197
|
+
|
|
198
|
+
if not interval_changed:
|
|
199
|
+
if self._interval_effect is None and new_interval is not None:
|
|
200
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if self._interval_effect is not None:
|
|
204
|
+
self._interval_effect.dispose()
|
|
205
|
+
self._interval_effect = None
|
|
206
|
+
|
|
207
|
+
if new_interval is not None:
|
|
208
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
209
|
+
|
|
155
210
|
# Reactive state
|
|
156
211
|
pages: ReactiveList[Page[T, TParam]]
|
|
157
212
|
error: Signal[Exception | None]
|
|
@@ -171,6 +226,9 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
171
226
|
|
|
172
227
|
_observers: "list[InfiniteQueryResult[T, TParam]]"
|
|
173
228
|
_gc_handle: asyncio.TimerHandle | None
|
|
229
|
+
_interval_effect: Effect | None
|
|
230
|
+
_interval: float | None
|
|
231
|
+
_interval_observer: "InfiniteQueryResult[T, TParam] | None"
|
|
174
232
|
|
|
175
233
|
def __init__(
|
|
176
234
|
self,
|
|
@@ -232,6 +290,9 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
232
290
|
self._queue_task = None
|
|
233
291
|
self._observers = []
|
|
234
292
|
self._gc_handle = None
|
|
293
|
+
self._interval_effect = None
|
|
294
|
+
self._interval = None
|
|
295
|
+
self._interval_observer = None
|
|
235
296
|
|
|
236
297
|
# ─────────────────────────────────────────────────────────────────────────
|
|
237
298
|
# Commit functions - update state after pages have been modified
|
|
@@ -326,13 +387,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
326
387
|
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
327
388
|
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
328
389
|
) -> 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))
|
|
390
|
+
"""Wait for any in-flight queue processing to complete."""
|
|
336
391
|
# Wait for any in-progress queue processing
|
|
337
392
|
if self._queue_task and not self._queue_task.done():
|
|
338
393
|
await self._queue_task
|
|
@@ -341,17 +396,31 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
341
396
|
return ActionError(cast(Exception, self.error()))
|
|
342
397
|
return ActionSuccess(list(self.pages))
|
|
343
398
|
|
|
399
|
+
async def ensure(
|
|
400
|
+
self,
|
|
401
|
+
fetch_fn: Callable[[TParam], Awaitable[T]] | None = None,
|
|
402
|
+
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
403
|
+
) -> ActionResult[list[Page[T, TParam]]]:
|
|
404
|
+
"""Ensure an initial fetch has started, then wait for completion."""
|
|
405
|
+
if len(self.pages) == 0 and self.status() == "loading":
|
|
406
|
+
if self._queue_task is None or self._queue_task.done():
|
|
407
|
+
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
408
|
+
self._enqueue(Refetch(fetch_fn=fn, observer=observer))
|
|
409
|
+
return await self.wait()
|
|
410
|
+
|
|
344
411
|
def observe(self, observer: Any):
|
|
345
412
|
self._observers.append(observer)
|
|
346
413
|
self.cancel_gc()
|
|
347
414
|
gc_time = getattr(observer, "_gc_time", 0)
|
|
348
415
|
if gc_time and gc_time > 0:
|
|
349
416
|
self.cfg.gc_time = max(self.cfg.gc_time, gc_time)
|
|
417
|
+
self._update_interval()
|
|
350
418
|
|
|
351
419
|
def unobserve(self, observer: "InfiniteQueryResult[T, TParam]"):
|
|
352
420
|
"""Unregister an observer. Cancels pending actions. Schedules GC if no observers remain."""
|
|
353
421
|
if observer in self._observers:
|
|
354
422
|
self._observers.remove(observer)
|
|
423
|
+
self._update_interval()
|
|
355
424
|
|
|
356
425
|
# Cancel pending actions from this observer
|
|
357
426
|
self._cancel_observer_actions(observer)
|
|
@@ -630,11 +699,17 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
630
699
|
(i for i, p in enumerate(self.pages) if p.param == action.param),
|
|
631
700
|
None,
|
|
632
701
|
)
|
|
633
|
-
if idx is None:
|
|
634
|
-
return None
|
|
635
702
|
|
|
636
703
|
page = await action.fetch_fn(action.param)
|
|
637
|
-
|
|
704
|
+
|
|
705
|
+
if idx is None:
|
|
706
|
+
# Page doesn't exist - jump to this page, clearing existing pages
|
|
707
|
+
self.pages.clear()
|
|
708
|
+
self.pages.append(Page(page, action.param))
|
|
709
|
+
else:
|
|
710
|
+
# Page exists, update it
|
|
711
|
+
self.pages[idx] = Page(page, action.param)
|
|
712
|
+
|
|
638
713
|
await self.commit()
|
|
639
714
|
return page
|
|
640
715
|
|
|
@@ -708,7 +783,10 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
708
783
|
cancel_fetch: bool = False,
|
|
709
784
|
) -> ActionResult[T | None]:
|
|
710
785
|
"""
|
|
711
|
-
Refetch
|
|
786
|
+
Refetch a page by its param. Queued for sequential execution.
|
|
787
|
+
|
|
788
|
+
If the page doesn't exist, clears existing pages and loads the requested
|
|
789
|
+
page as the new starting point.
|
|
712
790
|
|
|
713
791
|
Note: Prefer calling refetch_page() on InfiniteQueryResult to ensure the
|
|
714
792
|
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
@@ -725,6 +803,9 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
725
803
|
self._cancel_queue()
|
|
726
804
|
if self._queue_task and not self._queue_task.done():
|
|
727
805
|
self._queue_task.cancel()
|
|
806
|
+
if self._interval_effect is not None:
|
|
807
|
+
self._interval_effect.dispose()
|
|
808
|
+
self._interval_effect = None
|
|
728
809
|
if self.cfg.on_dispose:
|
|
729
810
|
self.cfg.on_dispose(self)
|
|
730
811
|
|
|
@@ -778,7 +859,6 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
778
859
|
_on_success: Callable[[list[Page[T, TParam]]], Awaitable[None] | None] | None
|
|
779
860
|
_on_error: Callable[[Exception], Awaitable[None] | None] | None
|
|
780
861
|
_observe_effect: Effect
|
|
781
|
-
_interval_effect: Effect | None
|
|
782
862
|
_data_computed: Computed[list[Page[T, TParam]] | None]
|
|
783
863
|
_enabled: Signal[bool]
|
|
784
864
|
_fetch_on_mount: bool
|
|
@@ -801,13 +881,17 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
801
881
|
self._fetch_fn = fetch_fn
|
|
802
882
|
self._stale_time = stale_time
|
|
803
883
|
self._gc_time = gc_time
|
|
804
|
-
|
|
884
|
+
interval = (
|
|
885
|
+
refetch_interval
|
|
886
|
+
if refetch_interval is not None and refetch_interval > 0
|
|
887
|
+
else None
|
|
888
|
+
)
|
|
889
|
+
self._refetch_interval = interval
|
|
805
890
|
self._keep_previous_data = keep_previous_data
|
|
806
891
|
self._on_success = on_success
|
|
807
892
|
self._on_error = on_error
|
|
808
893
|
self._enabled = Signal(enabled, name=f"inf_query.enabled({query().key})")
|
|
809
894
|
self._fetch_on_mount = fetch_on_mount
|
|
810
|
-
self._interval_effect = None
|
|
811
895
|
|
|
812
896
|
def observe_effect():
|
|
813
897
|
q = self._query()
|
|
@@ -816,8 +900,13 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
816
900
|
with Untrack():
|
|
817
901
|
q.observe(self)
|
|
818
902
|
|
|
819
|
-
if
|
|
820
|
-
|
|
903
|
+
# Skip if query interval is active - interval effect handles initial fetch
|
|
904
|
+
if enabled and fetch_on_mount and not q.has_interval:
|
|
905
|
+
# Fetch if no data loaded yet or if existing data is stale
|
|
906
|
+
if not q.is_fetching() and (
|
|
907
|
+
q.status() == "loading" or self.is_stale()
|
|
908
|
+
):
|
|
909
|
+
q.invalidate()
|
|
821
910
|
|
|
822
911
|
# Return cleanup function that captures the query (old query on key change)
|
|
823
912
|
def cleanup():
|
|
@@ -834,25 +923,6 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
834
923
|
self._data_computed_fn, name=f"inf_query_data({self._query().key})"
|
|
835
924
|
)
|
|
836
925
|
|
|
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,
|
|
854
|
-
)
|
|
855
|
-
|
|
856
926
|
@property
|
|
857
927
|
def status(self) -> QueryStatus:
|
|
858
928
|
return self._query().status()
|
|
@@ -985,15 +1055,20 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
985
1055
|
async def wait(self) -> ActionResult[list[Page[T, TParam]]]:
|
|
986
1056
|
return await self._query().wait(fetch_fn=self._fetch_fn, observer=self)
|
|
987
1057
|
|
|
1058
|
+
async def ensure(self) -> ActionResult[list[Page[T, TParam]]]:
|
|
1059
|
+
return await self._query().ensure(fetch_fn=self._fetch_fn, observer=self)
|
|
1060
|
+
|
|
988
1061
|
def invalidate(self):
|
|
989
1062
|
query = self._query()
|
|
990
1063
|
query.invalidate(fetch_fn=self._fetch_fn, observer=self)
|
|
991
1064
|
|
|
992
1065
|
def enable(self):
|
|
993
1066
|
self._enabled.write(True)
|
|
1067
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
994
1068
|
|
|
995
1069
|
def disable(self):
|
|
996
1070
|
self._enabled.write(False)
|
|
1071
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
997
1072
|
|
|
998
1073
|
def set_error(self, error: Exception):
|
|
999
1074
|
query = self._query()
|
|
@@ -1002,8 +1077,6 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
1002
1077
|
@override
|
|
1003
1078
|
def dispose(self):
|
|
1004
1079
|
"""Clean up the result and its observe effect."""
|
|
1005
|
-
if self._interval_effect is not None:
|
|
1006
|
-
self._interval_effect.dispose()
|
|
1007
1080
|
self._observe_effect.dispose()
|
|
1008
1081
|
|
|
1009
1082
|
|
|
@@ -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
|
...
|
|
@@ -268,6 +268,9 @@ class KeyedQuery(Generic[T], Disposable):
|
|
|
268
268
|
_task: asyncio.Task[None] | None
|
|
269
269
|
_task_initiator: "KeyedQueryResult[T] | None"
|
|
270
270
|
_gc_handle: asyncio.TimerHandle | None
|
|
271
|
+
_interval_effect: Effect | None
|
|
272
|
+
_interval: float | None
|
|
273
|
+
_interval_observer: "KeyedQueryResult[T] | None"
|
|
271
274
|
|
|
272
275
|
def __init__(
|
|
273
276
|
self,
|
|
@@ -293,6 +296,9 @@ class KeyedQuery(Generic[T], Disposable):
|
|
|
293
296
|
self._task = None
|
|
294
297
|
self._task_initiator = None
|
|
295
298
|
self._gc_handle = None
|
|
299
|
+
self._interval_effect = None
|
|
300
|
+
self._interval = None
|
|
301
|
+
self._interval_observer = None
|
|
296
302
|
|
|
297
303
|
# --- Delegate signal access to state ---
|
|
298
304
|
@property
|
|
@@ -438,6 +444,66 @@ class KeyedQuery(Generic[T], Disposable):
|
|
|
438
444
|
)
|
|
439
445
|
return self.observers[0]._fetch_fn # pyright: ignore[reportPrivateUsage]
|
|
440
446
|
|
|
447
|
+
@property
|
|
448
|
+
def has_interval(self) -> bool:
|
|
449
|
+
return self._interval is not None
|
|
450
|
+
|
|
451
|
+
def _select_interval_observer(
|
|
452
|
+
self,
|
|
453
|
+
) -> tuple[float | None, "KeyedQueryResult[T] | None"]:
|
|
454
|
+
min_interval: float | None = None
|
|
455
|
+
selected: "KeyedQueryResult[T] | None" = None
|
|
456
|
+
|
|
457
|
+
for obs in reversed(self.observers):
|
|
458
|
+
interval = obs._refetch_interval # pyright: ignore[reportPrivateUsage]
|
|
459
|
+
if interval is None:
|
|
460
|
+
continue
|
|
461
|
+
if not obs._enabled.value: # pyright: ignore[reportPrivateUsage]
|
|
462
|
+
continue
|
|
463
|
+
if min_interval is None or interval < min_interval:
|
|
464
|
+
min_interval = interval
|
|
465
|
+
selected = obs
|
|
466
|
+
|
|
467
|
+
return min_interval, selected
|
|
468
|
+
|
|
469
|
+
def _create_interval_effect(self, interval: float) -> Effect:
|
|
470
|
+
def interval_fn():
|
|
471
|
+
observer = self._interval_observer
|
|
472
|
+
if observer is None:
|
|
473
|
+
return
|
|
474
|
+
if not self.is_scheduled and len(self.observers) > 0:
|
|
475
|
+
self.run_fetch(
|
|
476
|
+
observer._fetch_fn, # pyright: ignore[reportPrivateUsage]
|
|
477
|
+
cancel_previous=False,
|
|
478
|
+
initiator=observer,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return Effect(
|
|
482
|
+
interval_fn,
|
|
483
|
+
name=f"query_interval({self.key})",
|
|
484
|
+
interval=interval,
|
|
485
|
+
immediate=True,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _update_interval(self) -> None:
|
|
489
|
+
new_interval, new_observer = self._select_interval_observer()
|
|
490
|
+
interval_changed = new_interval != self._interval
|
|
491
|
+
|
|
492
|
+
self._interval = new_interval
|
|
493
|
+
self._interval_observer = new_observer
|
|
494
|
+
|
|
495
|
+
if not interval_changed:
|
|
496
|
+
if self._interval_effect is None and new_interval is not None:
|
|
497
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
if self._interval_effect is not None:
|
|
501
|
+
self._interval_effect.dispose()
|
|
502
|
+
self._interval_effect = None
|
|
503
|
+
|
|
504
|
+
if new_interval is not None:
|
|
505
|
+
self._interval_effect = self._create_interval_effect(new_interval)
|
|
506
|
+
|
|
441
507
|
async def refetch(self, cancel_refetch: bool = True) -> ActionResult[T]:
|
|
442
508
|
"""
|
|
443
509
|
Reruns the query and returns the result.
|
|
@@ -468,11 +534,13 @@ class KeyedQuery(Generic[T], Disposable):
|
|
|
468
534
|
self.cancel_gc()
|
|
469
535
|
if observer._gc_time > 0: # pyright: ignore[reportPrivateUsage]
|
|
470
536
|
self.cfg.gc_time = max(self.cfg.gc_time, observer._gc_time) # pyright: ignore[reportPrivateUsage]
|
|
537
|
+
self._update_interval()
|
|
471
538
|
|
|
472
539
|
def unobserve(self, observer: "KeyedQueryResult[T]"):
|
|
473
540
|
"""Unregister an observer. Schedules GC if no observers remain."""
|
|
474
541
|
if observer in self.observers:
|
|
475
542
|
self.observers.remove(observer)
|
|
543
|
+
self._update_interval()
|
|
476
544
|
|
|
477
545
|
# If the departing observer initiated the ongoing fetch, cancel it
|
|
478
546
|
if self._task_initiator is observer and self._task and not self._task.done():
|
|
@@ -505,6 +573,9 @@ class KeyedQuery(Generic[T], Disposable):
|
|
|
505
573
|
def dispose(self):
|
|
506
574
|
"""Clean up the query, cancelling any in-flight fetch."""
|
|
507
575
|
self.cancel()
|
|
576
|
+
if self._interval_effect is not None:
|
|
577
|
+
self._interval_effect.dispose()
|
|
578
|
+
self._interval_effect = None
|
|
508
579
|
if self.cfg.on_dispose:
|
|
509
580
|
self.cfg.on_dispose(self)
|
|
510
581
|
|
|
@@ -559,7 +630,12 @@ class UnkeyedQueryResult(Generic[T], Disposable):
|
|
|
559
630
|
self._on_success = on_success
|
|
560
631
|
self._on_error = on_error
|
|
561
632
|
self._stale_time = stale_time
|
|
562
|
-
|
|
633
|
+
interval = (
|
|
634
|
+
refetch_interval
|
|
635
|
+
if refetch_interval is not None and refetch_interval > 0
|
|
636
|
+
else None
|
|
637
|
+
)
|
|
638
|
+
self._refetch_interval = interval
|
|
563
639
|
self._keep_previous_data = keep_previous_data
|
|
564
640
|
self._enabled = Signal(enabled, name="query.enabled(unkeyed)")
|
|
565
641
|
self._interval_effect = None
|
|
@@ -581,12 +657,13 @@ class UnkeyedQueryResult(Generic[T], Disposable):
|
|
|
581
657
|
|
|
582
658
|
# Schedule initial fetch if stale (untracked to avoid reactive loop)
|
|
583
659
|
with Untrack():
|
|
584
|
-
if
|
|
660
|
+
# Skip if refetch_interval is active - interval effect handles initial fetch
|
|
661
|
+
if enabled and fetch_on_mount and interval is None and self.is_stale():
|
|
585
662
|
self.schedule()
|
|
586
663
|
|
|
587
664
|
# Set up interval effect if interval is specified
|
|
588
|
-
if
|
|
589
|
-
self._setup_interval_effect(
|
|
665
|
+
if interval is not None:
|
|
666
|
+
self._setup_interval_effect(interval)
|
|
590
667
|
|
|
591
668
|
def _setup_interval_effect(self, interval: float):
|
|
592
669
|
"""Create an effect that invalidates the query at the specified interval."""
|
|
@@ -699,15 +776,18 @@ class UnkeyedQueryResult(Generic[T], Disposable):
|
|
|
699
776
|
return await self.wait()
|
|
700
777
|
|
|
701
778
|
async def wait(self) -> ActionResult[T]:
|
|
702
|
-
"""Wait for the current
|
|
703
|
-
# If loading and no task, schedule a fetch
|
|
704
|
-
if self.state.status() == "loading" and not self.state.is_fetching():
|
|
705
|
-
self.schedule()
|
|
779
|
+
"""Wait for the current in-flight fetch to complete."""
|
|
706
780
|
await self._effect.wait()
|
|
707
781
|
if self.state.status() == "error":
|
|
708
782
|
return ActionError(cast(Exception, self.state.error.read()))
|
|
709
783
|
return ActionSuccess(cast(T, self.state.data.read()))
|
|
710
784
|
|
|
785
|
+
async def ensure(self) -> ActionResult[T]:
|
|
786
|
+
"""Ensure an initial fetch has started, then wait for completion."""
|
|
787
|
+
if self.state.status() == "loading" and not self.state.is_fetching():
|
|
788
|
+
self.schedule()
|
|
789
|
+
return await self.wait()
|
|
790
|
+
|
|
711
791
|
def invalidate(self):
|
|
712
792
|
"""Mark the query as stale and refetch through the effect."""
|
|
713
793
|
if not self.is_scheduled:
|
|
@@ -740,7 +820,6 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
740
820
|
_on_success: Callable[[T], Awaitable[None] | None] | None
|
|
741
821
|
_on_error: Callable[[Exception], Awaitable[None] | None] | None
|
|
742
822
|
_observe_effect: Effect
|
|
743
|
-
_interval_effect: Effect | None
|
|
744
823
|
_data_computed: Computed[T | None]
|
|
745
824
|
_enabled: Signal[bool]
|
|
746
825
|
_fetch_on_mount: bool
|
|
@@ -762,12 +841,16 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
762
841
|
self._fetch_fn = fetch_fn
|
|
763
842
|
self._stale_time = stale_time
|
|
764
843
|
self._gc_time = gc_time
|
|
765
|
-
|
|
844
|
+
interval = (
|
|
845
|
+
refetch_interval
|
|
846
|
+
if refetch_interval is not None and refetch_interval > 0
|
|
847
|
+
else None
|
|
848
|
+
)
|
|
849
|
+
self._refetch_interval = interval
|
|
766
850
|
self._keep_previous_data = keep_previous_data
|
|
767
851
|
self._on_success = on_success
|
|
768
852
|
self._on_error = on_error
|
|
769
853
|
self._enabled = Signal(enabled, name=f"query.enabled({query().key})")
|
|
770
|
-
self._interval_effect = None
|
|
771
854
|
|
|
772
855
|
def observe_effect():
|
|
773
856
|
q = self._query()
|
|
@@ -776,9 +859,11 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
776
859
|
with Untrack():
|
|
777
860
|
q.observe(self)
|
|
778
861
|
|
|
779
|
-
#
|
|
780
|
-
if enabled and fetch_on_mount and
|
|
781
|
-
|
|
862
|
+
# Skip if query interval is active - interval effect handles initial fetch
|
|
863
|
+
if enabled and fetch_on_mount and not q.has_interval:
|
|
864
|
+
# If stale, schedule refetch (only when enabled)
|
|
865
|
+
if not q.is_fetching() and self.is_stale():
|
|
866
|
+
self.invalidate()
|
|
782
867
|
|
|
783
868
|
# Return cleanup function that captures the query (old query on key change)
|
|
784
869
|
def cleanup():
|
|
@@ -795,25 +880,6 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
795
880
|
self._data_computed_fn, name=f"query_data({self._query().key})"
|
|
796
881
|
)
|
|
797
882
|
|
|
798
|
-
# Set up interval effect if interval is specified
|
|
799
|
-
if refetch_interval is not None and refetch_interval > 0:
|
|
800
|
-
self._setup_interval_effect(refetch_interval)
|
|
801
|
-
|
|
802
|
-
def _setup_interval_effect(self, interval: float):
|
|
803
|
-
"""Create an effect that invalidates the query at the specified interval."""
|
|
804
|
-
|
|
805
|
-
def interval_fn():
|
|
806
|
-
# Read enabled to make this effect reactive to enabled changes
|
|
807
|
-
if self._enabled():
|
|
808
|
-
self.invalidate()
|
|
809
|
-
|
|
810
|
-
self._interval_effect = Effect(
|
|
811
|
-
interval_fn,
|
|
812
|
-
name=f"query_interval({self._query().key})",
|
|
813
|
-
interval=interval,
|
|
814
|
-
immediate=True,
|
|
815
|
-
)
|
|
816
|
-
|
|
817
883
|
@property
|
|
818
884
|
def status(self) -> QueryStatus:
|
|
819
885
|
return self._query().status()
|
|
@@ -874,9 +940,12 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
874
940
|
return await self.wait()
|
|
875
941
|
|
|
876
942
|
async def wait(self) -> ActionResult[T]:
|
|
877
|
-
"""Wait for the current
|
|
943
|
+
"""Wait for the current in-flight fetch to complete."""
|
|
944
|
+
return await self._query().wait()
|
|
945
|
+
|
|
946
|
+
async def ensure(self) -> ActionResult[T]:
|
|
947
|
+
"""Ensure an initial fetch has started, then wait for completion."""
|
|
878
948
|
query = self._query()
|
|
879
|
-
# If loading and no task, start a fetch with this observer's fetch function
|
|
880
949
|
if query.status() == "loading" and not query.is_fetching():
|
|
881
950
|
query.run_fetch(self._fetch_fn, initiator=self)
|
|
882
951
|
return await query.wait()
|
|
@@ -910,16 +979,16 @@ class KeyedQueryResult(Generic[T], Disposable):
|
|
|
910
979
|
def enable(self):
|
|
911
980
|
"""Enable the query."""
|
|
912
981
|
self._enabled.write(True)
|
|
982
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
913
983
|
|
|
914
984
|
def disable(self):
|
|
915
985
|
"""Disable the query, preventing it from fetching."""
|
|
916
986
|
self._enabled.write(False)
|
|
987
|
+
self._query()._update_interval() # pyright: ignore[reportPrivateUsage]
|
|
917
988
|
|
|
918
989
|
@override
|
|
919
990
|
def dispose(self):
|
|
920
991
|
"""Clean up the result and its observe effect."""
|
|
921
|
-
if self._interval_effect is not None and not self._interval_effect.__disposed__:
|
|
922
|
-
self._interval_effect.dispose()
|
|
923
992
|
if not self._observe_effect.__disposed__:
|
|
924
993
|
self._observe_effect.dispose()
|
|
925
994
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/pulse/__init__.py
RENAMED
|
File without changes
|
{pulse_framework-0.1.63 → pulse_framework-0.1.64}/src/pulse/transpiler/modules/pulse/tags.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|