Skip to content

Commit 22df424

Browse files
Clément VALENTINclaude
andcommitted
fix(simulator): use subscription pattern for cache hydration
The previous fix didn't fully address the race condition because queryClient.getQueryData() can return undefined before IndexedDB hydration completes. Now using the same hybrid pattern as other pages: useQuery to trigger cache entry + subscription to react to cache updates including IndexedDB hydration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b3dc580 commit 22df424

File tree

1 file changed

+67
-18
lines changed

1 file changed

+67
-18
lines changed

apps/web/src/pages/Simulator.tsx

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,56 @@ export default function Simulator() {
166166
// Selected PDL from global store
167167
const { selectedPdl, setSelectedPdl } = usePdlStore()
168168

169+
// HYBRID APPROACH for consumptionDetail: useQuery creates the cache entry for persistence,
170+
// but we read data via subscription to avoid race conditions with IndexedDB hydration
171+
const { isLoading: isConsumptionCacheLoading } = useQuery({
172+
queryKey: ['consumptionDetail', selectedPdl],
173+
queryFn: async () => {
174+
// Read from cache - this makes the query succeed with status: 'success'
175+
// Data is written to cache via setQueryData in useUnifiedDataFetch
176+
const cachedData = queryClient.getQueryData(['consumptionDetail', selectedPdl])
177+
return cachedData || null
178+
},
179+
enabled: !!selectedPdl,
180+
staleTime: Infinity, // Never refetch - data only comes from setQueryData
181+
gcTime: 1000 * 60 * 60 * 24 * 7, // 7 days
182+
refetchOnMount: false,
183+
refetchOnWindowFocus: false,
184+
refetchOnReconnect: false,
185+
})
186+
187+
// Read consumptionDetail data via direct cache access + subscription
188+
const [cachedConsumptionData, setCachedConsumptionData] = useState<any>(null)
189+
190+
useEffect(() => {
191+
if (!selectedPdl) {
192+
setCachedConsumptionData(null)
193+
return
194+
}
195+
196+
// Read current data from cache (includes persisted data after hydration)
197+
const initialData = queryClient.getQueryData(['consumptionDetail', selectedPdl])
198+
if (initialData) {
199+
logger.log('[Simulator] Initial cache data found:', !!initialData)
200+
setCachedConsumptionData(initialData)
201+
}
202+
203+
// Subscribe to future changes (when setQueryData is called or cache hydrates)
204+
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
205+
if (
206+
event?.type === 'updated' &&
207+
event?.query?.queryKey?.[0] === 'consumptionDetail' &&
208+
event?.query?.queryKey?.[1] === selectedPdl
209+
) {
210+
const updatedData = queryClient.getQueryData(['consumptionDetail', selectedPdl])
211+
logger.log('[Simulator] Cache updated:', !!updatedData)
212+
setCachedConsumptionData(updatedData)
213+
}
214+
})
215+
216+
return () => unsubscribe()
217+
}, [selectedPdl, queryClient])
218+
169219
// Simulation state
170220
const [isSimulating, setIsSimulating] = useState(false)
171221
const [simulationResult, setSimulationResult] = useState<any>(null)
@@ -213,18 +263,18 @@ export default function Simulator() {
213263
setIsInitializing(true)
214264
}, [selectedPdl])
215265

216-
// End initialization when required data is loaded (offers and providers)
266+
// End initialization when required data is loaded (offers, providers, and cache hydration)
217267
// This ensures auto-launch can check cache properly before showing empty state
218268
useEffect(() => {
219-
// Wait for offers and providers to finish loading
220-
if (!offersLoading && !providersLoading) {
221-
// Small delay to allow cache hydration to complete
269+
// Wait for all data sources to finish loading
270+
if (!offersLoading && !providersLoading && !isConsumptionCacheLoading) {
271+
// Small delay to allow any final state updates
222272
const timer = setTimeout(() => {
223273
setIsInitializing(false)
224274
}, 50)
225275
return () => clearTimeout(timer)
226276
}
227-
}, [selectedPdl, offersLoading, providersLoading])
277+
}, [selectedPdl, offersLoading, providersLoading, isConsumptionCacheLoading])
228278

229279
// Auto-collapse info section when simulation results are available
230280
useEffect(() => {
@@ -271,12 +321,11 @@ export default function Simulator() {
271321

272322
logger.log(`Loading consumption data from cache: ${startDate} to ${endDate}`)
273323

274-
// Get all data from the single cache key (new format)
275-
const cachedData = queryClient.getQueryData(['consumptionDetail', selectedPdl]) as any
324+
// Use cachedConsumptionData from state (already hydrated from IndexedDB via subscription)
276325
let allPoints: any[] = []
277326

278-
if (cachedData?.data?.meter_reading?.interval_reading) {
279-
const readings = cachedData.data.meter_reading.interval_reading
327+
if (cachedConsumptionData?.data?.meter_reading?.interval_reading) {
328+
const readings = cachedConsumptionData.data.meter_reading.interval_reading
280329

281330
// Filter readings to the desired date range (rolling year)
282331
allPoints = readings.filter((point: any) => {
@@ -364,7 +413,7 @@ export default function Simulator() {
364413
} finally {
365414
setIsSimulating(false)
366415
}
367-
}, [selectedPdl, pdlsData, offersData, providersData, queryClient])
416+
}, [selectedPdl, pdlsData, offersData, providersData, cachedConsumptionData])
368417

369418
const calculateSimulationsForAllOffers = (consumptionData: any[], offers: EnergyOffer[], providers: EnergyProvider[], tempoColors: TempoDay[], pdl?: PDL) => {
370419
// Create a map of date -> TEMPO color for fast lookup
@@ -781,6 +830,8 @@ export default function Simulator() {
781830
providersDataLoaded: !!providersData,
782831
offersLoading,
783832
providersLoading,
833+
isConsumptionCacheLoading,
834+
hasCachedData: !!cachedConsumptionData,
784835
})
785836

786837
// Don't auto-launch if already launched, simulating, or have results
@@ -789,8 +840,8 @@ export default function Simulator() {
789840
return
790841
}
791842

792-
// Don't auto-launch while data is still loading
793-
if (offersLoading || providersLoading) {
843+
// Don't auto-launch while data is still loading (including cache hydration)
844+
if (offersLoading || providersLoading || isConsumptionCacheLoading) {
794845
logger.log('[Auto-launch] Skipping auto-launch - still loading data')
795846
return
796847
}
@@ -805,15 +856,13 @@ export default function Simulator() {
805856
return
806857
}
807858

808-
// Check if we have cached data for this PDL (uses new single cache key format)
809-
const cachedData = queryClient.getQueryData(['consumptionDetail', selectedPdl]) as any
810-
811-
if (!cachedData?.data?.meter_reading?.interval_reading?.length) {
859+
// Use cachedConsumptionData from state (populated via subscription, handles IndexedDB hydration)
860+
if (!cachedConsumptionData?.data?.meter_reading?.interval_reading?.length) {
812861
logger.log('[Auto-launch] ❌ No cached data found for PDL:', selectedPdl)
813862
return
814863
}
815864

816-
const readings = cachedData.data.meter_reading.interval_reading
865+
const readings = cachedConsumptionData.data.meter_reading.interval_reading
817866
const totalPoints = readings.length
818867

819868
// Check if we have enough data (at least 30 days worth = ~1440 points at 30min intervals)
@@ -835,7 +884,7 @@ export default function Simulator() {
835884
} else {
836885
logger.log(`❌ Not enough cached data (${totalPoints} points), skipping auto-launch`)
837886
}
838-
}, [selectedPdl, isSimulating, simulationResult, hasAutoLaunched, isDemo, pdlsData, offersData, providersData, offersLoading, providersLoading, queryClient, handleSimulation])
887+
}, [selectedPdl, isSimulating, simulationResult, hasAutoLaunched, isDemo, pdlsData, offersData, providersData, offersLoading, providersLoading, isConsumptionCacheLoading, cachedConsumptionData, handleSimulation])
839888

840889
// Filter and sort simulation results
841890
// IMPORTANT: This hook must be before any early returns to respect React's rules of hooks

0 commit comments

Comments
 (0)