RSMS results prefetch and cancellation — design
This document records agreed design decisions before implementation. It covers background loading of scenario-scoped results APIs so top-bar views (especially Maps) are not contending with redundant or badly ordered work, without requiring ResultsPage to remount on every visit.
Problem
DashboardLayout keeps several top-bar views mounted at once (Maps, Street View, Hydraulics, Results, Barge Traffic), toggling visibility by route. ResultsPage therefore runs its data effects even when another tab is visible, which issues many results API calls as soon as basin + scenario are available.
Maps also calls results-related endpoints (e.g. plume river-mile axis for merged Ohio GeoJSON). That can duplicate network work and saturate the browser’s connection pool, so the first interaction with Maps feels slow even though the map component itself does not “await” Results in code.
Goals
- Prefetch scenario-scoped results metadata (and optionally heavier payloads) as soon as a scenario is reasonably knowable — e.g. when the user selects a scenario on Scenarios, not only after opening Maps or Results.
- Cancel or ignore work when the user switches scenario (or basin) before requests complete; never commit stale data to UI state.
- Avoid duplicate fetches across consumers (Results, Maps plume merge, prefetch runner) via a shared cache keyed by
(riverbasin_id, scenario_id). - Do not rely on “mount
ResultsPageonly when/dashboard/resultsis active” as the primary strategy, so in-page state (sliders, toggles, last selections) survives tab switches without a full cold reload.
Non-goals
- Changing backend shapes or adding new endpoints (unless later discovered necessary).
- Mandatory service workers or offline persistence.
- Replacing the stacked top-bar layout entirely (only data-loading behavior is in scope).
Context (current architecture)
- Global selection:
selectedRiverbasinId,selectedScenarioinGlobalProvider/useSelectedRiverbasin(). - Results API access:
rsms-frontend-react/src/api/results.jsandget()inrsms-frontend-react/src/api/client.js(GET only in initial scope). - Map merge hook:
useMergedOhioMapDatafetches static GeoJSON plusgetPlumeRiverMileAxiswhen basin + scenario exist.
Finalized design decisions
1. Prefetch start trigger
Prefetch may start as soon as selectedRiverbasinId and selectedScenario.id are set — for example immediately after selection on the Scenarios page. Nothing requires a top-bar route to mount first.
Constraints:
- Prefetch must respect authentication the same way existing calls do (no token races).
- Prefetch should be non-blocking by default: schedule work asynchronously (e.g. after the current task, or
requestIdleCallbackwhere appropriate) so selection UI stays responsive.
2. Keep ResultsPage mounted; integrate with cache instead of unmounting
We will not adopt “render ResultsPage only when the Results tab is active” as the main fix. Subsequent visits to Results should reuse warm state where possible.
Because the page stays mounted while other tabs are visible, we must still coordinate:
- Prefer shared cache / in-flight promises so Results effects do not blindly refetch the same resources the prefetch just completed.
- Optionally defer heavier series loads until Results is foregrounded or idle, if profiling shows hidden-tab work still hurts Maps (policy layer on top of cache, not mandatory for v1).
3. Shared cache and deduplication
Introduce a small client-side results cache (module or context):
- Key: stable string from
(riverbasinId, scenarioId)(and optionally aresourcename: e.g.plumeRiverMileAxis,cxpltHoursDomain, …). - Value: resolved JSON (or error sentinel) plus optional in-flight
Promisededuplication so concurrent callers share one network request. - Eviction: bounded size (e.g. LRU for last N scenarios) to cap memory; exact N chosen at implementation time.
Maps: useMergedOhioMapData should use getOrFetch for plume river-mile axis (or equivalent) so Results and Maps do not duplicate that GET for the same scenario.
4. Cancellation and stale-response safety
Two layers (both recommended for v1):
Logical generation / version: increment a monotonic counter (or derive from
scenarioId+ sequential token) whenever basin or scenario changes. Every async completion checks “still current?” before callingsetStateor writing cache entries for that scenario.AbortSignal: extendrequest/getinapi/client.jsto forward an optionalsignaltofetch. The prefetch orchestrator passes a controller that isabort()when the user selects a different scenario (or clears selection).
Note: aborted fetches reject; callers must distinguish abort from real errors so UI does not flash misleading error toasts.
5. HTTP GET scope
Initial implementation targets GET results endpoints only. Other verbs can adopt signal later for consistency.
Suggested implementation phases (ordered)
- Cache + deduping
getlayer for results resources used by Maps and Results (plume axis first, then domain endpoints as needed). - AbortSignal support in
client.js/results.js, wired into prefetch and Results loaders. - Prefetch orchestrator (provider or hook near
GlobalProvider/ dashboard root): on scenario selection, enqueue domain/light calls first, then heavier payloads; honor generation + abort. - Refactor
ResultsPage(anduseMergedOhioMapData) to read-through cache and avoid duplicate parallel fetches with prefetch.
Risks and mitigations
| Risk | Mitigation |
|---|---|
Hidden ResultsPage still fires effects and doubles bandwidth | Cache + shared in-flight promises; optional deferral for heavy series when tab hidden |
| Fast scenario switching shows wrong data briefly | Generation check before every state update; clear per-scenario loading flags |
| Memory growth from caching large payloads | LRU cap; cache metadata first if full series too large |
| Abort treated as user-visible error | Catch AbortError / DOMException name and skip error UI |
Open items
Resolved for v1: prefetch includes domains + CX at hour 0 + CT at mile 0 + edge resolve + mass; LRU holds two scenarios storing full JSON per keyed response. Tab-visibility deferral remains optional — see rsms-master-tasklist if profiling shows it is worthwhile.
Implementation (v1)
src/utils/resultsScenarioBundleCache.js— LRU capacity 2 (RESULTS_SCENARIO_CACHE_CAPACITY). Stores full parsed JSON per resource key on a scenario bundle: CX/CT domains, plume river-mile axis,cxpltByHour(per hour cache key),ctpltByMile(per mile key), resolved edge envelope, mass balance. In-flight dedupe viapendingField.PREFETCH_INITIAL_CX_HOUR = 0,PREFETCH_INITIAL_CT_MILES = 0— first slider steps for prefetch (aligned withcxpltDomainAllowsHourZero/ctpltDomainAllowsMilesZeroonResultsPagewhen setting defaults).src/components/ResultsScenarioPrefetch.jsx— mounted inApp.jsxunderSelectedRiverbasinProvider; startsprefetchScenarioResultsBundlewhen authenticated and basin + scenario are set;AbortControlleraborts prefetch when selection changes or component unmounts.get()/fetchinapi/client.js— optionalsignalpassed through;results.jsGET helpers accept{ signal }.ResultsPage.jsx,useMergedOhioMapData.js,RsmsStationCtpltPanel.jsx— results reads go throughcachedGet*helpers so Maps and prefetch share cached bytes.
Stale writes after LRU eviction during an in-flight request are benign: getOrPopulateField assigns into the bundle only if the Map entry still exists for that key.
References (code)
rsms-frontend-react/src/layouts/dashboard/index.jsx— stacked top-bar views.rsms-frontend-react/src/pages/ResultsPage.jsx— scenario-scoped results effects.rsms-frontend-react/src/pages/map/useMergedOhioMapData.js— plume axis for map layer.rsms-frontend-react/src/api/results.js,rsms-frontend-react/src/api/client.js— API access.rsms-frontend-react/src/utils/resultsScenarioBundleCache.js,rsms-frontend-react/src/components/ResultsScenarioPrefetch.jsx— LRU cache + background prefetch.