Summary
Every conversation_updated WebSocket frame invalidates the whole conversations-eager query, which re-drains every page of every server from scratch.
Real servers emit these liveness pings continuously, so the eager conversations drain runs in a near-continuous loop, which keeps the Hub in the "Showing cached data — syncing" state and re-rendering indefinitely.
Root cause
app/_layout.tsx (around line 158) registers wsManager.onAll('conversation_updated', ...) and calls refreshEagerConversations(queryClient) on every frame.
refreshEagerConversations in lib/eagerCacheSync.ts is just queryClient.invalidateQueries({ queryKey: ['conversations-eager'] }).
Invalidating conversations-eager re-runs its queryFn, which fetches /api/conversations/count and then every page (50 per page) for every displayed server, sequentially per server.
There is no debounce or coalescing, so a burst of frames triggers a burst of full multi-server re-drains, and a steady trickle of frames keeps a drain almost always in flight.
Evidence (measured on-device)
Measured on the iOS simulator with two real servers paired and EXPO_PUBLIC_OPEN_TRACE=1.
A per-commit "why did this render" probe on ProjectsHub showed convLoaded, convTotal, and convCounting changing repeatedly, and convCounting flipping about 113 times in one sampling window.
convCounting only flips when the eager queryFn restarts and resets progress to { loaded: 0, total: 0 }, so ~113 flips means the query re-ran ~113 times.
A temporary counter added to refreshEagerConversations logged about 2 invalidations per 18 seconds at rest, and far more during the servers' initial warm-up scan, matching the observed render rate (~6/sec during warm-up, ~1.3/sec at rest).
Each re-drain also fires a burst of recordReady calls, which compounds the separate serverFetchStatus re-render issue tracked in the sibling bug.
Impact
The Hub re-renders and refetches continuously while any server is emitting liveness pings, which is most of the time for active servers.
This wastes network (a full multi-server pagination per ping) and CPU (sustained re-renders), and is a primary reason the "syncing" state feels slow.
Proposed fixes
Short term: debounce or coalesce refreshEagerConversations (for example a trailing debounce of ~1–2 seconds) so a burst of conversation_updated frames collapses into a single re-drain.
Long term: patch only the affected conversation's entry in the eager cache instead of re-draining every page of every server, mirroring how applySessionUpdateToEagerCache updates a single session in place for session_update frames.
Context
Surfaced while verifying PR #563 (coalesce eager-fetch progress + memoize list roots) on-device.
The throttle and memo in that PR cannot stop this loop because the driver is repeated query invalidation, not per-page progress bursts.
Summary
Every
conversation_updatedWebSocket frame invalidates the wholeconversations-eagerquery, which re-drains every page of every server from scratch.Real servers emit these liveness pings continuously, so the eager conversations drain runs in a near-continuous loop, which keeps the Hub in the "Showing cached data — syncing" state and re-rendering indefinitely.
Root cause
app/_layout.tsx(around line 158) registerswsManager.onAll('conversation_updated', ...)and callsrefreshEagerConversations(queryClient)on every frame.refreshEagerConversationsinlib/eagerCacheSync.tsis justqueryClient.invalidateQueries({ queryKey: ['conversations-eager'] }).Invalidating
conversations-eagerre-runs itsqueryFn, which fetches/api/conversations/countand then every page (50 per page) for every displayed server, sequentially per server.There is no debounce or coalescing, so a burst of frames triggers a burst of full multi-server re-drains, and a steady trickle of frames keeps a drain almost always in flight.
Evidence (measured on-device)
Measured on the iOS simulator with two real servers paired and
EXPO_PUBLIC_OPEN_TRACE=1.A per-commit "why did this render" probe on ProjectsHub showed
convLoaded,convTotal, andconvCountingchanging repeatedly, andconvCountingflipping about 113 times in one sampling window.convCountingonly flips when the eagerqueryFnrestarts and resets progress to{ loaded: 0, total: 0 }, so ~113 flips means the query re-ran ~113 times.A temporary counter added to
refreshEagerConversationslogged about 2 invalidations per 18 seconds at rest, and far more during the servers' initial warm-up scan, matching the observed render rate (~6/sec during warm-up, ~1.3/sec at rest).Each re-drain also fires a burst of
recordReadycalls, which compounds the separateserverFetchStatusre-render issue tracked in the sibling bug.Impact
The Hub re-renders and refetches continuously while any server is emitting liveness pings, which is most of the time for active servers.
This wastes network (a full multi-server pagination per ping) and CPU (sustained re-renders), and is a primary reason the "syncing" state feels slow.
Proposed fixes
Short term: debounce or coalesce
refreshEagerConversations(for example a trailing debounce of ~1–2 seconds) so a burst ofconversation_updatedframes collapses into a single re-drain.Long term: patch only the affected conversation's entry in the eager cache instead of re-draining every page of every server, mirroring how
applySessionUpdateToEagerCacheupdates a single session in place forsession_updateframes.Context
Surfaced while verifying PR #563 (coalesce eager-fetch progress + memoize list roots) on-device.
The throttle and memo in that PR cannot stop this loop because the driver is repeated query invalidation, not per-page progress bursts.