Skip to content

fix: runtime bugs and lifecycle gaps from code review - #62

Merged
psachmann merged 6 commits into
mainfrom
fix/fabel-review-findings
Jul 26, 2026
Merged

fix: runtime bugs and lifecycle gaps from code review#62
psachmann merged 6 commits into
mainfrom
fix/fabel-review-findings

Conversation

@psachmann

Copy link
Copy Markdown
Owner

Summary

Addresses all findings from the fabel code review (FINDINGS.md/REVIEW.md).

High severity

  • SetArgs no longer leaks an orphaned Query (with a live background fetch loop) on every cache hit — the losing candidate is now disposed, mirroring PrefetchAsync.
  • Cancel() no longer permanently bricks the query — the CancellationTokenSource is recreated under lock instead of being cancelled once and reused forever.
  • Value-type TData is now handled correctly: QueryState<TData> tracks presence via explicit HasData/HasLastData flags instead of null-checks, and QueryOptions.InitialData is now an Optional<TData> so default(TData) (e.g. 0) is distinguishable from "not set". Transition.razor uses the explicit flags instead of ??.

Medium severity

  • RefetchInterval now routes through Invalidate(), so it respects the subscriber-count and stale-time gates instead of fetching forever with nobody listening.
  • Cache entries are now evicted automatically when the last State subscriber leaves (and the pending eviction is cancelled if a new one joins), closing the gap where entries only left the cache via manual Detach().
  • QueryCache.Remove now holds its lock for the whole method and disposes any overwritten pending-removal subscription, closing a race with GetOrCreate and a double-Detach leak. Also guards against CacheTime values too large for the scheduler's underlying timer to represent.
  • QueryRefreshMonitor.js now exports unregister() to remove its listeners, and both DevTools/RefreshMonitor DisposeAsync methods catch JSDisconnectedException from normal circuit teardown.

Minor cleanup: consistent locking around _lastSuccessAt, disposed-guards on Refetch/Invalidate, suppressed a spurious Idle emission from a superseded fetch, TryAdd in AddDotNetQuery, a cached JsonSerializerOptions instance, a descriptive error on query-key type collisions, an unforgeable QueryKey.Default sentinel, and doc notes on Success/Failure replay semantics.

psachmann and others added 3 commits July 6, 2026 12:07
High severity:
- SetArgs no longer leaks an orphaned Query (with a live background fetch
  loop) on every cache hit; the losing candidate is disposed like
  PrefetchAsync already did.
- Cancel() no longer permanently bricks the query — the CancellationTokenSource
  is recreated under lock instead of being cancelled once and reused forever.
- Value-type TData is now handled correctly: QueryState<TData> tracks presence
  via explicit HasData/HasLastData flags instead of null-checks, and
  QueryOptions.InitialData is now an Optional<TData> so default(TData) (e.g. 0)
  is distinguishable from "not set". Transition.razor uses the explicit flags.

Medium severity:
- RefetchInterval now routes through Invalidate(), so it respects the
  subscriber-count and stale-time gates instead of fetching forever with
  nobody listening.
- Cache entries are now evicted automatically when the last State subscriber
  leaves (and the pending eviction is cancelled if a new one joins), closing
  the gap where entries only left the cache via manual Detach().
- QueryCache.Remove now holds its lock for the whole method and disposes any
  overwritten pending-removal subscription, closing a race with GetOrCreate
  and a double-Detach leak. Also guards against CacheTime values too large
  for the scheduler's underlying timer to represent.
- QueryRefreshMonitor.js now exports unregister() to remove its listeners,
  and both DevTools/RefreshMonitor DisposeAsync methods catch
  JSDisconnectedException from normal circuit teardown.

Minor cleanup: consistent locking around _lastSuccessAt, disposed-guards on
Refetch/Invalidate, suppressed a spurious Idle emission from a superseded
fetch, TryAdd in AddDotNetQuery, a cached JsonSerializerOptions instance, a
descriptive error on query-key type collisions, an unforgeable QueryKey.Default
sentinel, and doc notes on Success/Failure replay semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FINDINGS.md and REVIEW.md were scratch output from the fabel review; their
content is now addressed and belongs in the PR description, not the repo.

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

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.70130% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.98%. Comparing base (72a73da) to head (e4548b0).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...DotNetQuery.Blazor.DevTools/QueryDevTools.razor.cs 71.42% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #62      +/-   ##
==========================================
+ Coverage   94.56%   94.98%   +0.41%     
==========================================
  Files          21       21              
  Lines         773      877     +104     
  Branches      133      150      +17     
==========================================
+ Hits          731      833     +102     
- Misses         19       21       +2     
  Partials       23       23              

☔ 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.

psachmann and others added 2 commits July 26, 2026 18:39
…ng presence

Simplifies the value-type TData fix by constraining TData to class on
IQuery<TArgs,TData>, QueryOptions<TArgs,TData>, QueryState<TData>, and the
internal Query/QueryObserver/QueryCache/QueryClient plumbing, plus Suspense
and Transition. Reference types are the overwhelmingly common case for query
results (protobuf messages, HTTP API DTOs), and constraining TData lets the
library rely on null as a reliable "no data yet" sentinel again instead of
threading explicit presence flags through every state transition.

Removes the Optional<TData> wrapper and QueryState's HasData/HasLastData
tracking added for the previous fix, reverting to plain nullable checks.
Mutations are unaffected — MutationState already derived HasData from
Status rather than a null-check, so they never had this bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@psachmann psachmann changed the title Fix runtime bugs and lifecycle gaps from code review ficx: runtime bugs and lifecycle gaps from code review Jul 26, 2026
@psachmann psachmann changed the title ficx: runtime bugs and lifecycle gaps from code review fix: runtime bugs and lifecycle gaps from code review Jul 26, 2026
Raises patch coverage by exercising code paths introduced in the review-fix
commits that weren't yet hit by any test: Query.Invalidate/Cancel after
Dispose, QueryCache.GetOrCreate's type-collision guard, Remove() called
twice before its eviction timer fires, QueryKey.Default's marker ToString,
and QueryRefreshMonitor.DisposeAsync's full body plus its
JSDisconnectedException catch. Also fixes the shared bUnit JS module setup
to call .SetVoidResult() — SetupVoid alone left the mocked Task pending,
invisible until a test actually awaited disposal end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@psachmann
psachmann merged commit 5f1d1fc into main Jul 26, 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