Skip to content

feat(query+observability): Infinite queries + observability alignment - #68

Merged
psachmann merged 10 commits into
mainfrom
feat/infinite-query-observability
Aug 10, 2026
Merged

feat(query+observability): Infinite queries + observability alignment#68
psachmann merged 10 commits into
mainfrom
feat/infinite-query-observability

Conversation

@psachmann

Copy link
Copy Markdown
Owner

What's new

Core: IInfiniteQuery<TArgs, TData, TPageParam>

  • Created via IQueryClient.CreateInfiniteQuery, following the same proxy-plus-cache-entry split as regular queries: InfiniteQueryObserver is the key-switching proxy handed to callers, InfiniteQuery is the cached entry, and both share the one QueryCache with regular queries (same key held by a different entry shape throws).
  • Accumulates pages via three commands — Refetch (re-fetches every loaded page), FetchNextPage, FetchPreviousPage — with cursors derived by GetNextPageParam / GetPreviousPageParam returning PageParam<T>.Some(...) or .None.
  • Same semantics as regular queries: Switch()-based cancellation of superseded fetches, stale-while-revalidate (invalidation with no subscribers defers the fetch to the first subscriber), Cancel() without poisoning the entry, MaxPages trimming, InitialData seeding, and retry via IRetryHandler.
  • TData is constrained to reference types, matching IQuery<TArgs, TData> (null means "no data yet").
  • DataComparer preserves page instances across refetches when pages are structurally unchanged, and Success suppresses duplicate emissions via element-wise page comparison.

Blazor: <InfiniteSuspense> / <InfiniteTransition>

  • The IInfiniteQuery counterparts of <Suspense> / <Transition>. Their Content slot receives the whole InfiniteQueryState, so templates can render Pages alongside HasNextPage / IsFetchingNextPage / IsFetchingPreviousPage.

Observability

  • Infinite fetches emit the same query.fetch span as regular queries, tagged with direction (refetch_all / next / previous) and query.pages; a refetch-all runs under a single span with retry-aware attempts accounting.
  • Fetch spans now also carry query.name for trace↔metric correlation.
  • ⚠️ Breaking (observability): duration histograms (dotnetquery.query.duration, dotnetquery.mutation.duration) now record seconds per OTel semantic conventions (previously milliseconds). Dashboards and alert thresholds need rescaling. Log messages still report ms.
  • dotnetquery.cache.entries is now decremented on cache dispose, so scoped SSR clients no longer permanently inflate the process-wide gauge.

psachmann and others added 9 commits June 13, 2026 12:22
…e observability stack

Brings the infinite-query implementation up to the query lifecycle and telemetry
model introduced on main (#62, #63). InfiniteQuery was written against the older
Query, so merging the branches cleanly required reconciling both sides.

Cache:
- Generalize QueryCache.GetOrCreate over a new internal ICacheEntry rather than
  Query<TArgs, TData>, so infinite queries share one cache with regular queries.
  ICacheEntry carries the Subscribed/Unsubscribed signals the cache uses to drive
  eviction; IQueryInspector stays the DevTools-facing surface, which observers
  also implement.
- Keep main's typed-entry guard, now reported per cache-entry shape.
- Infinite queries consequently take part in subscriber-driven eviction, which
  they previously did not — only an explicit Detach() removed them.

InfiniteQuery:
- Adopt the current instrumentation API: MetricName tagging, trigger tags, and
  attempt counts. Retries are accumulated per page, so a refetch-all of N pages
  reports 1 attempt when every page succeeds first try.
- Tag the fetch span with the new `direction` tag (refetch_all / next / previous).
- Route RefetchInterval through Invalidate() so polling respects StaleTime and
  defers to stale-while-revalidate when no subscribers are attached.
- Swap in a fresh CancellationTokenSource on Cancel(), so cancelling no longer
  permanently poisons the query.
- Only roll state back on the explicit Cancel() path; a fetch superseded by
  Switch() no longer stomps the newer fetch's state.
- Guard Refetch/FetchNextPage/FetchPreviousPage against disposal, and read
  _lastSuccessAt under the lock.

Also adds an optional Name to InfiniteQueryOptions for metric tagging, and
documents the direction tag and refetch-all attempt semantics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Correct the formatter command: csharpier 1.3.0 requires the `format`
  subcommand, bare `dotnet csharpier .` only prints help. CI's `check .`
  was already correct.
- Document infinite queries across the architecture notes: the client
  factory method, the InfiniteQueryObserver/InfiniteQuery pair, and the
  Blazor components.
- Document the ICacheEntry vs IQueryInspector split and the metric-name
  tagging rules, since both are easy to get wrong when adding a query type.
- Ignore ./coverage, which the CI test command writes to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…vability

- Constrain TData to reference types across the infinite query surface;
  an unconstrained struct page made InitialData silently seed default(TData)
- Implement DataComparer: unchanged pages keep their previous instance on
  refetch, and Success suppresses re-emissions via element-wise comparison
- Resolve page params once per run (replaces ShouldFetch) so user delegates
  run once, and only record fetch success when a fetch actually ran
- Return a snapshot from InfiniteQuery.CurrentData instead of a live view
- Decrement dotnetquery.cache.entries on cache dispose so scoped SSR
  clients no longer leak into the process-wide gauge
- Record duration histograms in seconds per OTel semantic conventions
  (logs stay in ms); add query.name to fetch spans and query.pages to
  infinite fetch spans
- Correct InfiniteSuspense failure-slot docs; add bUnit tests for
  InfiniteSuspense/InfiniteTransition and tests for the new behaviors

Full review and resolution notes in REVIEW-infinite-query-observability.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.74074% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.69%. Comparing base (44be2ed) to head (ed72ec5).

Files with missing lines Patch % Lines
src/DotNetQuery.Core/Internals/InfiniteQuery.cs 89.44% 13 Missing and 29 partials ⚠️
...otNetQuery.Core/Internals/InfiniteQueryObserver.cs 89.90% 2 Missing and 9 partials ⚠️
src/DotNetQuery.Blazor/InfiniteTransition.razor 83.33% 0 Missing and 3 partials ⚠️
src/DotNetQuery.Blazor/InfiniteSuspense.razor 88.88% 0 Missing and 2 partials ⚠️
src/DotNetQuery.Core/InfiniteQueryState.cs 98.07% 1 Missing ⚠️
src/DotNetQuery.Core/Internals/Query.cs 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #68      +/-   ##
==========================================
- Coverage   94.27%   92.69%   -1.58%     
==========================================
  Files          22       29       +7     
  Lines         995     1602     +607     
  Branches      179      275      +96     
==========================================
+ Hits          938     1485     +547     
- Misses         21       37      +16     
- Partials       36       80      +44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@psachmann psachmann self-assigned this Aug 10, 2026
Codecov's patch coverage for this PR was 84.72%, with InfiniteQuery.cs at
86.18%, InfiniteQueryObserver.cs at 76.14%, and QueryClient.cs's new
CreateInfiniteQuery lines at 0% patch coverage (never exercised through
the client, only through the observer/query directly). Add tests that
target the specific miss/partial lines Codecov's API reported:

- QueryClientTests: exercise CreateInfiniteQuery directly (fetch,
  cache-sharing) instead of only via InfiniteQueryObserver
- InfiniteQueryObserverTests: cover the previously-untested observer
  surface (Key, CacheTime, Status, CurrentData, LastUpdatedAt,
  ObserverCount, MetricName, StateChanged, SetEnabled, FetchPreviousPage,
  Failure, Cancel, Invalidate, Detach, idempotent Dispose, and the
  candidate.Dispose() cache-hit branch in SetArgs)
- InfiniteQueryTests: Status/LastUpdatedAt/ObserverCount property access,
  CurrentData null with no pages, a retrying IRetryHandler to cover the
  FetchPageAsync retry-counting branch, and a TCS-controlled fetch to
  hit the disposed-mid-fetch "didFetch = false" path without throwing
- InfiniteSuspenseTests/InfiniteTransitionTests: add the
  OnParametersSet_SameQueryInstance_DoesNotResubscribe regression test
  already present for Suspense/Transition, covering the early-return
  branch when the Query parameter reference is unchanged

417 tests passing (was 391), verified stable across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@psachmann
psachmann merged commit 285ce62 into main Aug 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant