Refresh Stats on resume so the latest data appears automatically#23112
Refresh Stats on resume so the latest data appears automatically#23112jkmassel wants to merge 3 commits into
Conversation
The Subscribers, Insights, and Traffic stats use cases are @singleton, so their in-memory domainModel lives for the whole app process. Nothing refetched when the screen was re-displayed — onResume didn't refresh, and start() no-ops against an already-SUCCESS singleton. Once a stale or incomplete result was loaded (e.g. during a server-side stats aggregation lag), it was pinned and re-shown on every return with no network call, so server-side recovery never surfaced without a manual pull-to-refresh or a cold start. Trigger the existing non-forced refresh from StatsListFragment.onResume(). The store-level STALE_PERIOD (5 min) throttles this to at most one network request per 5 minutes, and the 50ms updateState debounce keeps within-window resumes from flashing a loading state.
2197627 to
fa3e1dc
Compare
|
|
|
|
…ceholder The onResume refresh (and pull-to-refresh) drive blocks through a LOADING state during the network round-trip. mapSubscribers and mapStatsWithOverview rendered LOADING from stateData only (the loading placeholder = a bare title), and mapInsights used `stateData ?: data` — but stateData is always non-null (buildLoadingItem returns a title), so all three discarded the already-loaded rows and blanked the blocks for the duration of the fetch. Prefer data over the placeholder on LOADING (data ?: stateData ?: listOf()), matching StatsViewAllViewModel. First load still shows the placeholder (no data yet); a refresh of already-loaded data keeps the rows on screen. Add regression tests for mapSubscribers.
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |
| // path, so StatsRequestSqlUtils.STALE_PERIOD throttles it to at most one network request per | ||
| // 5 minutes; resumes within that window are served from cache. | ||
| if (::viewModel.isInitialized) { | ||
| viewModel.onRefresh() |
There was a problem hiding this comment.
⛏️ The first run will be making two refresh calls now. One in viewmodel.start() called from onViewCreated()and one in onResume()
On first display the initial start() load (from onViewCreated) and the onResume refresh both fire and race to the network before either records the StatsRequest timestamp, so STALE_PERIOD doesn't dedup them. Add a per-use-case AtomicBoolean guard in BaseStatsUseCase.fetch: a second concurrent non-forced fetch is skipped while one is in flight. A forced refresh (pull-to-refresh) still proceeds so it can bypass STALE_PERIOD. Keeps offscreen tab preloading (start() unchanged) while removing the first-load double-fetch; also covers other load races.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #23112 +/- ##
=======================================
Coverage 37.63% 37.64%
=======================================
Files 2343 2343
Lines 127290 127297 +7
Branches 17655 17658 +3
=======================================
+ Hits 47903 47917 +14
+ Misses 75464 75451 -13
- Partials 3923 3929 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|


Description
Should resolve https://linear.app/a8c/issue/CMM-2141.
Summary
Root cause
The Subscribers/Insights/Traffic stats use cases are
@Singleton(StatsModuleBLOCK_SUBSCRIBERS_USE_CASES→SUBSCRIBERS_USE_CASE, and equivalents), soBaseStatsUseCase.domainModellives for the entire app process. Nothing refetched on re-display:StatsListFragment.onResume()didn't refresh — only pull-to-refresh did.StatsListViewModel.start()short-circuits onisInitialized, and itsloadData(refresh = false)no-ops against an already-SUCCESSsingleton.So once a stale/incomplete result was loaded — e.g. during a server-side email/subscriber stats aggregation lag (CMM-2141), where a recently-published post is temporarily missing from
stats/emails/summary— that result was pinned in memory and re-shown on every return with no network call. Server-side recovery never surfaced until the user pulled to refresh or the process was killed.Fix
Refresh on resume —
StatsListFragment.onResume()now callsviewModel.onRefresh():if (::viewModel.isInitialized) { viewModel.onRefresh() }onResumefires on every foreground / tab return, so the refresh is deliberately non-forced and leans on the store-levelSTALE_PERIOD(5 min,StatsRequestSqlUtils) to throttle to at most one network request per 5 minutes per stat type. Applies to all sections hosted byStatsListFragment.Keep rows visible during the refresh — a
refresh=truefetch drives blocks through aLOADINGstate for the network round-trip.mapSubscribers/mapStatsWithOverviewrenderedLOADINGfromstateDataonly, andmapInsightsusedstateData ?: data— butstateData(the placeholder frombuildLoadingItem()) is always non-null, so all three discarded the already-loaded rows and blanked the block to a bare title during every network refresh. They now preferdata(data ?: stateData ?: listOf()), matchingStatsViewAllViewModel: first load still shows the placeholder, but refreshing loaded data keeps the rows on screen. (Within-5-min resumes are collapsed entirely by the 50 msupdateStatedebounce, so there's no visible change at all.)Boundary — what this does NOT fix
This does not touch the server-side lag itself (CMM-2141 root cause): if the user returns while
stats/emails/summaryis still missing the post, the refetch returns the same incomplete list. It shrinks the client staleness window from "until manual refresh / process death" to "next resume, ≤5 min throttled," so once the server recovers the app reflects it automatically.Testing instructions
Stats refresh on return:
No regression to pull-to-refresh:
Unit tests:
UiModelMapperTest—mapSubscribers keeps loaded rows visible while a block is refreshing+ the first-load placeholder case.Related