perf(desktop): de-block thread aux from first paint, page 500, cache reopens - #6446
Open
wpfleger96 wants to merge 2 commits into
Open
perf(desktop): de-block thread aux from first paint, page 500, cache reopens#6446wpfleger96 wants to merge 2 commits into
wpfleger96 wants to merge 2 commits into
Conversation
…reopens A 300-reply thread cold-open paid four serial relay legs before anything painted: two content pages (limit 200) followed by two aux waves for edits/deletions/reactions over all reply ids. staleTime:0 re-ran the whole pipeline on every reopen. Raise THREAD_PAGE_LIMIT to the server-clamped 500 so a <=500-reply thread fetches its content in one page. Resolve loadThreadReplies with content as soon as the page loop completes and hydrate aux into the same thread-replies cache via a functional setQueryData merge, off the critical path -- the exact pattern the channel timeline already ships. Set a 30s staleTime so reopening a recently-loaded thread is a cache hit; the live subscription keeps the subscribed channel's cache fresh while the bound guarantees an unsubscribed thread refetches to pick up edits/deletions it missed. The merge folds aux over whatever content is current, so a live append that lands mid-flight is preserved and a message a later refetch dropped is never resurrected. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The suite exercised backfillThreadAux() in isolation but never ran the loader through the de-block seam, so it stayed green if the fire-and-forget dispatch regressed to an await (recreating the first-paint latency this change removes) or was dropped entirely. Make loadThreadReplies injectable at the relay boundary and add behavioral tests proving content resolves while aux is pending, aux merges into the thread cache after content resolves, and aux rejection leaves the content query successful. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
hayt/thread-fetch-quick-wins
branch
from
August 21, 2026 02:08
ddfd716 to
0981d68
Compare
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.
Opening a long thread (~200-300 replies) in the desktop app was slow and flaky, and reopening the same thread re-ran the entire fetch from scratch. This is PR-2 of the desktop thread-load arc — the fetch-pipeline quick wins.
What was slow
A 300-reply cold open paid four serial relay legs before anything painted:
THREAD_PAGE_LIMITwas 200, so 300 replies took two serialget_thread_repliescalls.withThreadAuxfetched edits/deletions and reactions over all ~301 ids and blocked the resolve on them.staleTime: 0meant closing and reopening the panel re-ran the whole pipeline every time.Changes
THREAD_PAGE_LIMIT200 → 500. The bridge clamps a thread page toBRIDGE_THREAD_MAX_LIMIT(500) and the Tauri command caps it with.min(500), so 500 is the largest page the server serves — a ≤500-reply thread now cold-opens its content in a single round trip.loadThreadRepliesresolves with content replies as soon as the page loop completes. Structural aux (edits/deletions) and reactions hydrate asynchronously into the samethread-repliescache via a functionalsetQueryDatamerge — the exact pattern the channel timeline already ships (backfillAuxForMessages). Accepted tradeoff: an edited reply may briefly render its original text until the merge lands.staleTime0 → 30s onuseThreadRepliesanduseThreadRepliesForRoots, so reopening a recently-loaded thread is a cache hit. While the panel's channel is subscribed, the live WS subscription writes every new content and aux event into the thread-replies key (hooks.tsappendMessage), keeping the warm cache current. The finite bound exists precisely because that self-healing only covers the subscribed channel: edits/deletions/reactions arriving for a thread whose channel is not subscribed never reach the cache, so a 30sstaleTimeguarantees the next mount refetches and corrects them.Safety
The async merge uses a functional
setQueryDataupdater keyed on the thread's(channelId, rootId), so it can only touch that thread's cache. It folds aux over whatever content is current rather than a fetch-time snapshot, so a live reply that appends mid-flight is preserved, and aux referencing a message a later refetch dropped simply renders against nothing — content is never resurrected. Both aux fetches stay best-effort: a failure logs and degrades to bare replies.Tests
Extends
useThreadReplies.test.mjs: aux merges into the content cache; both-fetches-fail degrades to bare replies; the merge folds over a live mid-flight append rather than the stale snapshot; and the page-limit/staleTime invariants are pinned.