Repository navigation
fix(web): stop cached queries from keeping closed views in memory - #9020
Conversation
Why: FullCalendar fires datesSet from its own render, outside any Solid owner. The handler spread every root option to find datesSet, which ran each prop getter; conditional props compile to memos, so every date change left memos that were never disposed. They stayed subscribed to calendar settings and kept closed calendar panes alive. What: read only the datesSet prop in the handler.
Why: useThreadOperations built its queryFn inside the hook, closing over the thread view's threadId accessor and hook state. TanStack keeps the last options on the cached query for gcTime (10 minutes) after the view closes, so every opened email kept its whole pane, DOM included, alive that long. What: build the options in a module-scope factory over the plain thread ID, and read the previous result from the cache instead of hook state.
Why: usePendingGraphqlSoupDone defined its userInfo select and doneSession initialData inline. Both queries live for the whole session and keep the options of their last observer, so those callbacks retained the hook's scope and, through its query proxies, the whole closed channel or thread that last mounted the hook. On device, closed channel views stayed in memory after GC. What: pass module-scope callbacks (viewerIdFromInfo, createDoneSessionId) so the cached options no longer close over the hook.
Why: ItemPreview passes its own props as the preview entity, and the cached REST preview queryFn closed over that object. Each closed channel message with an entity preview stayed reachable through the query cache until gcTime. What: itemPreviewQueryOptions copies the id, type, and message ID into a plain request object before building the queryFn.
Why: The mentions menu (in every channel composer) and the form booking step defined their scheduling settings and public profile queryFns inline. The cached callbacks retained the hook scope, keeping closed channels and forms alive until gcTime. What: module-scope query option factories over the resolved scope, team, and profile IDs.
|
No actionable comments were generated in the recent review.
|
Why
On iOS, the native app sometimes reloads from scratch. Each reload is iOS killing WebKit's WebContent process for going over its memory limit (
ActiveSoft 1536 MBon an iPhone 12 mini). In the last 7 days, the most-affected user had 42 kills, 24 of them within 10 minutes of the previous restart. In more than half of their kills, and of the next-most-affected user's, a channel was open.On an iPhone 12 mini with a production account, opening the same 13 Home items twice grew WebContent from 699 MB to 1264 MB. After a forced GC, 7 closed channel panes and 14,309 detached elements were still in memory.
Heap snapshots traced most of those panes to TanStack Query. A cached query keeps its last observer's options. So a
queryFn,selectorinitialDatacreated inside a hook keeps that hook's scope alive after the view closes, and through the scope's query proxies and Solid owner it keeps the pane's DOM too. That lasts until the query is collected: 10 minutes by default, and never for the session-long queries.How it works
Each holder's cached callbacks move to module-scope option factories that take plain IDs and flags, as the "Query callback lifetimes" section of
.agents/rules/web/data.mdalready asks. The calendar fix is separate: an unowned-memo leak in the FullCalendar wrapper.With all five fixes, the same device and account retained 3 closed panes after about 24 opens, down from 7. Memory grew about 290–390 MB, down from 565 MB. The Home items differ between runs, so the MB figures vary by roughly ±100 MB, and the pane count is the more reliable measure.
What
lib/queries/soup/graphql/optimistic-done.ts:usePendingGraphqlSoupDonepasses module-scopeviewerIdFromInfoandcreateDoneSessionIdas the["auth","userInfo"]selectand thedoneSessioninitialData. Both queries live for the whole session, so the last channel or thread to mount the hook was retained indefinitely. This was the largest holder.lib/queries/preview/preview.ts:itemPreviewQueryOptionscopiesid,typeandmessageIdinto a plain request object.ItemPreviewpasses its own props as the entity, so the cached RESTqueryFnretained the message that contained the mention chip.MentionsMenu/hooks/useBookingLinkMention.ts(in every composer's mentions menu) andfeatures/block-form/queries/booking-sources.ts: the scheduling settings and public-profile queries come from module-scope factories.features/email-thread/queries/thread-operations.ts: the operationsqueryFncloses over the thread ID only and reads the previous result from the cache instead of hook state.lib/fullcalendar-solid/FullCalendar.tsx: FullCalendar callsdatesSetoutside any Solid owner. The handler spread every root option to find it, which turned conditional prop getters into memos that were never disposed and kept closed calendar panes alive. It now reads only thedatesSetprop.Follow-ups