Repository navigation
perf(fetchium): quiet query reactivation and focus-aware polling - #70
Merged
abedell-phantom merged 13 commits intoOct 8, 2026
Merged
Conversation
This was referenced Oct 5, 2026
abedell-phantom
marked this pull request as ready for review
October 5, 2026 14:11
abedell-phantom
added this pull request to stack #72
October 5, 2026 19:51
Adds opt-in client options, all defaulting to current behavior: - reactivationGraceMs (+ per-query override): a reactivating query whose data is younger than the grace is not refetched. Data a running subscription kept current counts as fresh until deactivation. Invalidation, refetch(), network reconnects and failed fetches still refetch. - reactivationStaggerMs: reactivation refetches queued in the same task are spread evenly over the window; adapters that set coalescesRequests keep theirs together. - activity + pollResumeJitterMs: poll() stops timers while the app is inactive and spreads overdue ticks (including ticks that fire over a second late after frozen timers) across the jitter window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion delivered The grace counted any running subscription as keeping the data current until the query deactivated. A poll() that never ticked qualified: it restarts its full interval on every activation, so visits shorter than the interval separated by gaps shorter than the grace chained the grace forever and never refetched (poll 60 s, staleTime 0, grace 10 s: twenty 50 s visits served 18-minute-old data from one fetch). The same credit covered time a focus-aware poll spent stopped in the background. Now only a delivery counts: a stream event through the subscription's onEvent, or a topic event the adapter sends with its topic (sendMutationEvent(event, topic)), and only up to the last one. A poll() keeps data current through the fetches it makes, which already move updatedAt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uring the window A staggered refetch waits up to reactivationStaggerMs and then started unconditionally. A refetch() or poll tick in that window was aborted by it and repeated, and a fetch that had already landed was repeated too. It now starts only if no fetch has started since the query was queued. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ions This change had no changeset, so its new QueryClient options would have been missing from the release notes. No source change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
abedell-phantom
force-pushed
the
perf/quiet-reactivation
branch
from
October 6, 2026 17:01
3403472 to
a7fcbd3
Compare
- __fetchNext() counts as a fetch start, so a staggered refetch waiting in the window no longer resets the page it loaded. - An aborted fetch that resolves late no longer clears lastFetchFailed, which may belong to the fetch that replaced it. - A reconnect while the query was inactive now voids the grace, using a new NetworkManager.reconnects counter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion # Conflicts: # packages/fetchium/src/QueryResult.ts
…activation Skip the reactivation grace when a Signal param changed while inactive, and let a debounced fetch (e.g. invalidateQueries()) scheduled before the stagger flush drop the query's stagger slot instead of being replaced by it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jimmy-phantom
approved these changes
Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Returning to a screen reactivated all of its queries at once. Each one refetched if it was past
staleTime, even when its data was seconds old or a live subscription had just updated it. All those refetches started in the same task, andpoll()kept running while the app was in the background. In a large React Native app this made tab returns and app resumes bursty.What changed
Every option here is opt-in. With defaults, behavior is unchanged.
reactivationGraceMsonQueryClient, with a per-query override in the query config. A query that reactivates with data younger than the grace is not refetched.subscribestream event, or a topic event sent with its topic.poll()subscription counts only through the fetches it makes.markStale(),refetch(), network reconnects and a failed last fetch still refetch.TopicQueryAdapter.sendMutationEvent(event, topic?): pass the topic so that topic's query gets credit for the push.reactivationStaggerMs: reactivation refetches queued in the same task are spread evenly across the window, in activation order.QueryAdapter.coalescesRequests: an adapter that sets it keeps its queries' reactivation refetches together, so they can share a request.activity: { isActive(), subscribe(listener) }onQueryClient:poll()stops while inactive and resumes when active. A tick not yet due keeps its original time.pollResumeJitterMs, with a per-pollpoll({ interval, resumeJitterMs })override: a tick overdue on resume, or one whose timer fired more than 1 s late, is rescheduled at a random point in the window.ActivitySource.Behavior changes
None with default options.
Known limitations
AppStatein anActivitySource.sendMutationEvent. Otherwise the grace measures from the last fetch.reactivationStaggerMsset, the stagger flush runs one task after reactivation.Testing
reactivation-grace.test.ts: grace defaults and per-query override, invalidation, failed fetches, push credit for streams, polls and topics, reconnects, stagger order, coalescing adapters, skipped and cancelled staggered refetches.poll-activity.test.ts: stop and resume, jitter, late ticks, listener cleanup.react/pause-reactivation.test.tsx: resuming a paused screen with and without the grace.topic-batch-delivery.test.ts: topic snapshots delivered in one task settle in one flush.Changeset
minor:.changeset/quiet-reactivation.md, added during review so these options appear in the release notes.Stack: #67 → #64 → #61 → #69 → #70 (this) → #71.
🤖 Generated with Claude Code