Skip to content

Commit 4631798

Browse files
Clément VALENTINclaude
andcommitted
fix(simulator): fix cache hydration race condition with IndexedDB
- Add useIsRestoring hook to wait for React Query IndexedDB hydration - Split cache subscription into two effects: immediate subscription + post-hydration read - Ensure offersData and providersData are always arrays to prevent undefined errors - Restore docker-compose port mappings for frontend (8000:5173) and backend (8081:8000) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 219b10f commit 4631798

File tree

2 files changed

+36
-55
lines changed

2 files changed

+36
-55
lines changed

apps/web/src/pages/Simulator.tsx

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect, useMemo, useCallback } from 'react'
22
import { Calculator, AlertCircle, Loader2, ChevronDown, ChevronUp, FileDown, ArrowUpDown, ArrowUp, ArrowDown, Filter, Info, ArrowRight } from 'lucide-react'
3-
import { useQuery, useQueryClient } from '@tanstack/react-query'
3+
import { useQuery, useQueryClient, useIsRestoring } from '@tanstack/react-query'
44
import { LoadingOverlay } from '@/components/LoadingOverlay'
55
import { LoadingPlaceholder } from '@/components/LoadingPlaceholder'
66
import { AnimatedSection } from '@/components/AnimatedSection'
@@ -123,6 +123,7 @@ function calcPrice(quantity: number | undefined, price: string | number | undefi
123123
export default function Simulator() {
124124
// const { user } = useAuth() // Unused for now
125125
const queryClient = useQueryClient()
126+
const isRestoring = useIsRestoring()
126127
const isDemo = useIsDemo()
127128

128129
// Fetch user's PDLs
@@ -141,7 +142,7 @@ export default function Simulator() {
141142
})
142143

143144
// Fetch energy providers and offers
144-
const { data: providersData, isLoading: providersLoading } = useQuery({
145+
const { data: providersDataRaw, isLoading: providersLoading } = useQuery({
145146
queryKey: ['energy-providers'],
146147
queryFn: async () => {
147148
const response = await energyApi.getProviders()
@@ -150,9 +151,13 @@ export default function Simulator() {
150151
}
151152
return []
152153
},
154+
staleTime: 0,
153155
})
154156

155-
const { data: offersData, isLoading: offersLoading } = useQuery({
157+
// Ensure providersData is always an array
158+
const providersData = Array.isArray(providersDataRaw) ? providersDataRaw : []
159+
160+
const { data: offersDataRaw, isLoading: offersLoading } = useQuery({
156161
queryKey: ['energy-offers'],
157162
queryFn: async () => {
158163
const response = await energyApi.getOffers()
@@ -161,8 +166,12 @@ export default function Simulator() {
161166
}
162167
return []
163168
},
169+
staleTime: 0, // Always refetch to ensure fresh data
164170
})
165171

172+
// Ensure offersData is always an array
173+
const offersData = Array.isArray(offersDataRaw) ? offersDataRaw : []
174+
166175
// Selected PDL from global store
167176
const { selectedPdl, setSelectedPdl } = usePdlStore()
168177

@@ -187,35 +196,41 @@ export default function Simulator() {
187196
// Read consumptionDetail data via direct cache access + subscription
188197
const [cachedConsumptionData, setCachedConsumptionData] = useState<any>(null)
189198

199+
// Subscribe to cache updates IMMEDIATELY (don't wait for hydration)
200+
// This ensures we capture setQueryData events even while hydrating
190201
useEffect(() => {
191202
if (!selectedPdl) {
192203
setCachedConsumptionData(null)
193204
return
194205
}
195206

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)
207+
// Subscribe to future changes (when setQueryData is called)
204208
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
209+
// Listen to 'updated' events for this query (setQueryData triggers 'updated')
205210
if (
206211
event?.type === 'updated' &&
207212
event?.query?.queryKey?.[0] === 'consumptionDetail' &&
208213
event?.query?.queryKey?.[1] === selectedPdl
209214
) {
210215
const updatedData = queryClient.getQueryData(['consumptionDetail', selectedPdl])
211-
logger.log('[Simulator] Cache updated:', !!updatedData)
212216
setCachedConsumptionData(updatedData)
213217
}
214218
})
215219

216220
return () => unsubscribe()
217221
}, [selectedPdl, queryClient])
218222

223+
// Read initial data from cache AFTER hydration completes
224+
useEffect(() => {
225+
if (!selectedPdl || isRestoring) return
226+
227+
// Hydration complete - read current data from cache
228+
const initialData = queryClient.getQueryData(['consumptionDetail', selectedPdl])
229+
if (initialData) {
230+
setCachedConsumptionData(initialData)
231+
}
232+
}, [selectedPdl, queryClient, isRestoring])
233+
219234
// Simulation state
220235
const [isSimulating, setIsSimulating] = useState(false)
221236
const [simulationResult, setSimulationResult] = useState<any>(null)
@@ -819,60 +834,25 @@ export default function Simulator() {
819834
// Auto-launch simulation if cache data exists
820835
// IMPORTANT: This hook must be before any early returns to respect React's rules of hooks
821836
useEffect(() => {
822-
logger.log('[Auto-launch] useEffect triggered', {
823-
selectedPdl,
824-
isSimulating,
825-
hasSimulationResult: !!simulationResult,
826-
hasAutoLaunched,
827-
isDemo,
828-
pdlsDataLoaded: !!pdlsData,
829-
offersDataLoaded: !!offersData,
830-
providersDataLoaded: !!providersData,
831-
offersLoading,
832-
providersLoading,
833-
isConsumptionCacheLoading,
834-
hasCachedData: !!cachedConsumptionData,
835-
})
836-
837837
// Don't auto-launch if already launched, simulating, or have results
838-
if (!selectedPdl || isSimulating || simulationResult || hasAutoLaunched) {
839-
logger.log('[Auto-launch] Skipping auto-launch due to conditions')
840-
return
841-
}
838+
if (!selectedPdl || isSimulating || simulationResult || hasAutoLaunched) return
842839

843840
// Don't auto-launch while data is still loading (including cache hydration)
844-
if (offersLoading || providersLoading || isConsumptionCacheLoading) {
845-
logger.log('[Auto-launch] Skipping auto-launch - still loading data')
846-
return
847-
}
841+
if (offersLoading || providersLoading || isConsumptionCacheLoading) return
848842

849843
// Don't auto-launch if PDL data, offers, or providers are not loaded yet
850-
if (!pdlsData || !Array.isArray(offersData) || offersData.length === 0 || !providersData) {
851-
logger.log('[Auto-launch] Skipping auto-launch - data not loaded yet', {
852-
pdlsData: !!pdlsData,
853-
offersData: Array.isArray(offersData) ? offersData.length : 'not array',
854-
providersData: !!providersData
855-
})
856-
return
857-
}
844+
if (!pdlsData || offersData.length === 0 || providersData.length === 0) return
858845

859846
// Use cachedConsumptionData from state (populated via subscription, handles IndexedDB hydration)
860-
if (!cachedConsumptionData?.data?.meter_reading?.interval_reading?.length) {
861-
logger.log('[Auto-launch] ❌ No cached data found for PDL:', selectedPdl)
862-
return
863-
}
847+
if (!cachedConsumptionData?.data?.meter_reading?.interval_reading?.length) return
864848

865849
const readings = cachedConsumptionData.data.meter_reading.interval_reading
866850
const totalPoints = readings.length
867851

868852
// Check if we have enough data (at least 30 days worth = ~1440 points at 30min intervals)
869853
const minRequiredPoints = 30 * 48 // 30 days * 48 half-hours
870-
const hasEnoughData = totalPoints >= minRequiredPoints
871-
872-
logger.log(`[Auto-launch] Cache check: ${totalPoints} points (need ${minRequiredPoints} minimum)`)
873854

874-
if (hasEnoughData) {
875-
logger.log(`✅ Auto-launching simulation with ${totalPoints} cached points`)
855+
if (totalPoints >= minRequiredPoints) {
876856
setHasAutoLaunched(true)
877857
// Show loading overlay while preparing simulation
878858
setIsInitialLoadingFromCache(true)
@@ -881,8 +861,6 @@ export default function Simulator() {
881861
setTimeout(() => {
882862
handleSimulation()
883863
}, 100)
884-
} else {
885-
logger.log(`❌ Not enough cached data (${totalPoints} points), skipping auto-launch`)
886864
}
887865
}, [selectedPdl, isSimulating, simulationResult, hasAutoLaunched, isDemo, pdlsData, offersData, providersData, offersLoading, providersLoading, isConsumptionCacheLoading, cachedConsumptionData, handleSimulation])
888866

docker-compose.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ services:
5757
context: ./apps/api
5858
dockerfile: Dockerfile
5959
restart: unless-stopped
60+
ports:
61+
- "8081:8000"
6062
env_file:
6163
- ./.env.api
6264
environment:
@@ -100,7 +102,8 @@ services:
100102
- ./apps/web/tailwind.config.js:/app/tailwind.config.js
101103
- ./apps/web/postcss.config.js:/app/postcss.config.js
102104
- /app/node_modules
103-
# ports définis dans docker-compose.override.yml pour support multi-instances
105+
ports:
106+
- "8000:5173"
104107
networks:
105108
- myelectricaldata
106109
depends_on:

0 commit comments

Comments
 (0)