refactor(map): fix race conditions and async issues with atomic GPS/Data fetch

This commit is contained in:
2026-03-02 19:22:58 -05:00
parent 4f8096f425
commit 767667b1b6
3 changed files with 61 additions and 37 deletions

View File

@ -27,7 +27,7 @@ export const useFlujoPrincipal = () => {
const procesarSeleccionDeRuta = async ( const procesarSeleccionDeRuta = async (
_ruta: { id: string }, _ruta: { id: string },
paradas: BusStop[], paradasExistentes: BusStop[],
map: google.maps.Map | undefined, map: google.maps.Map | undefined,
addCleanMarker: Function, addCleanMarker: Function,
skipGuidedZoom = false, skipGuidedZoom = false,
@ -36,16 +36,32 @@ export const useFlujoPrincipal = () => {
if (!map) return if (!map) return
try { try {
// ── PASO 1: Limpiar todo lo que había antes ────────── // ── PASO 1: Inicio Atómico ──
limpiarMapa()
cargando.value = true cargando.value = true
limpiarMapa()
const routeStore = useRouteStore()
// ── PASO 2: Obtener ubicación ── // ── PASO 2: Sincronización (Promise.all) ──
// Disparamos Supabase (paradas) y GPS al mismo tiempo
console.log('SIBU | Iniciando carga síncrona de Datos + GPS...');
const [ubicacionRes, paradasRes] = await Promise.allSettled([
obtenerUbicacion(),
paradasExistentes.length > 0 ? Promise.resolve(paradasExistentes) : routeStore.loadRouteStops(_ruta.id)
]);
// ── PASO 3: Asignación Segura ──
let ubicacion: { lat: number, lng: number } | null = null; let ubicacion: { lat: number, lng: number } | null = null;
try { if (ubicacionRes.status === 'fulfilled') {
ubicacion = await obtenerUbicacion(); ubicacion = ubicacionRes.value;
} catch (err) { } else {
console.warn('SIBU | No se pudo obtener ubicación', err); console.warn('SIBU | GPS falló o fue denegado');
}
let paradas: BusStop[] = [];
if (paradasRes.status === 'fulfilled') {
// Si loadRouteStops no devolviera los datos directamente, los tomamos del store
paradas = paradasRes.value || routeStore.selectedRouteStops;
} }
const paradasFormateadas = paradas.map((p, i) => ({ const paradasFormateadas = paradas.map((p, i) => ({
@ -56,17 +72,24 @@ export const useFlujoPrincipal = () => {
orden: i orden: i
})); }));
if (paradasFormateadas.length < 2) return; if (paradasFormateadas.length < 2) {
console.warn('SIBU | No hay suficientes paradas para trazar ruta');
if (!ubicacion) {
await trazarRuta(paradasFormateadas, map, false);
return; return;
} }
// ── PASO 3: Dibujar ruta completa (fondo, gris tenue) ─ // Si no detectamos GPS, trazamos la ruta completa sin zoom guiado
if (!ubicacion) {
await trazarRuta(paradasFormateadas, map, false);
const bounds = new google.maps.LatLngBounds()
paradasFormateadas.forEach(p => bounds.extend(new google.maps.LatLng(p.latitud, p.longitud)))
map.fitBounds(bounds, { top: 100, bottom: 100, left: 60, right: 60 })
return;
}
// ── PASO 4: Dibujar y Renderizar ──
// Dibujar ruta completa (fondo)
await trazarRuta(paradasFormateadas, map, true); await trazarRuta(paradasFormateadas, map, true);
// ── PASO 4: Calcular parada más cercana (Omitir si skipGuidedZoom) ───────────────
if (skipGuidedZoom) { if (skipGuidedZoom) {
const bounds = new google.maps.LatLngBounds() const bounds = new google.maps.LatLngBounds()
paradasFormateadas.forEach(p => bounds.extend(new google.maps.LatLng(p.latitud, p.longitud))) paradasFormateadas.forEach(p => bounds.extend(new google.maps.LatLng(p.latitud, p.longitud)))
@ -78,10 +101,7 @@ export const useFlujoPrincipal = () => {
const paradaCercanaFound = paradaCercana.value const paradaCercanaFound = paradaCercana.value
if (!paradaCercanaFound) return; if (!paradaCercanaFound) return;
const routeStore = useRouteStore() // Dibujar tramo relevante
if (routeStore.selectedRouteId !== _ruta.id) return;
// ── PASO 5: Dibujar tramo relevante (Amarillo Vivo) ───────
const idx = paradasFormateadas.findIndex(p => { const idx = paradasFormateadas.findIndex(p => {
const samePos = Math.abs(p.longitud - paradaCercanaFound.longitude) < 0.0001 && const samePos = Math.abs(p.longitud - paradaCercanaFound.longitude) < 0.0001 &&
Math.abs(p.latitud - paradaCercanaFound.latitude) < 0.0001; Math.abs(p.latitud - paradaCercanaFound.latitude) < 0.0001;
@ -95,7 +115,7 @@ export const useFlujoPrincipal = () => {
} }
} }
// ── PASO 6: Dibujar marcadores de todas las paradas ────────── // Renderizado Condicional de Marcadores
paradasFormateadas.forEach((p, i) => { paradasFormateadas.forEach((p, i) => {
const esCercana = i === idx; const esCercana = i === idx;
const esPasada = idx !== -1 && i < idx; const esPasada = idx !== -1 && i < idx;
@ -112,13 +132,13 @@ export const useFlujoPrincipal = () => {
); );
}); });
// ── PASO 7: Zoom centrado en usuario + parada cercana ─
hacerZoomAlTramoRelevante(ubicacion, paradaCercanaFound, map) hacerZoomAlTramoRelevante(ubicacion, paradaCercanaFound, map)
} catch (error) { } catch (error) {
console.error('SIBU | Error procesando ruta:', error) console.error('SIBU | Error procesando ruta:', error)
errorMsg.value = 'No se pudo cargar la ruta' errorMsg.value = 'No se pudo cargar la ruta'
} finally { } finally {
// Apagar estado de carga
cargando.value = false cargando.value = false
} }
} }

View File

@ -43,29 +43,31 @@ export const useRouteStore = defineStore('route', () => {
} }
} }
async function loadRouteStops(routeId: string, force = false) { async function loadRouteStops(routeId: string, force = false): Promise<BusStop[]> {
const CACHE_TIME = 1000 * 60 * 15; // 15 minutos const CACHE_TIME = 1000 * 60 * 15; // 15 minutos
const now = Date.now(); const now = Date.now();
if (isLoadingStops.value) return; if (stopsCache.value.has(routeId) && !force) {
if (!force && stopsCache.value.has(routeId)) {
const cacheEntry = stopsCache.value.get(routeId)!; const cacheEntry = stopsCache.value.get(routeId)!;
if (now - cacheEntry.fetchedAt < CACHE_TIME) { if (now - cacheEntry.fetchedAt < CACHE_TIME) {
selectedRouteStops.value = cacheEntry.stops; selectedRouteStops.value = cacheEntry.stops;
return; return cacheEntry.stops;
} }
} }
if (isLoadingStops.value) return [];
isLoadingStops.value = true isLoadingStops.value = true
error.value = null error.value = null
try { try {
const stops = await routesService.getRouteStops(routeId) const stops = await routesService.getRouteStops(routeId)
selectedRouteStops.value = stops selectedRouteStops.value = stops
stopsCache.value.set(routeId, { fetchedAt: now, stops }) stopsCache.value.set(routeId, { fetchedAt: now, stops })
return stops
} catch (e) { } catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load route stops' error.value = e instanceof Error ? e.message : 'Failed to load route stops'
console.error('Error loading route stops:', e) console.error('Error loading route stops:', e)
selectedRouteStops.value = [] selectedRouteStops.value = []
return []
} finally { } finally {
isLoadingStops.value = false isLoadingStops.value = false
} }
@ -75,8 +77,7 @@ export const useRouteStore = defineStore('route', () => {
if (selectedRouteId.value === routeId) return if (selectedRouteId.value === routeId) return
selectedRouteId.value = routeId selectedRouteId.value = routeId
selectedRouteName.value = routeName selectedRouteName.value = routeName
selectedRouteStops.value = [] // Clear old stops immediately selectedRouteStops.value = [] // Limpia para forzar recarga atómica en la vista
await loadRouteStops(routeId)
} }
function clearSelection() { function clearSelection() {

View File

@ -37,7 +37,7 @@ const { estasCargando: estasCargandoRuta, errorRuta } = useDirectionsRoute();
const { encontrarParadaCercana, paradaCercana, distanciaMetros, duracionCaminata, limpiarCaminata } = useParadaCercana(); const { encontrarParadaCercana, paradaCercana, distanciaMetros, duracionCaminata, limpiarCaminata } = useParadaCercana();
const { calcularETA, busesActivos, cargando: etaCargando } = useETA(); const { calcularETA, busesActivos, cargando: etaCargando } = useETA();
const { procesarSeleccionDeRuta } = useFlujoPrincipal(); const { procesarSeleccionDeRuta, cargando: flujoCargando } = useFlujoPrincipal();
const { limpiarMapa: limpiarTodoCentralizado } = useMapState(); const { limpiarMapa: limpiarTodoCentralizado } = useMapState();
const showETACard = ref(false); const showETACard = ref(false);
@ -237,20 +237,22 @@ function reDrawUserMarker() {
async function updateMapMarkers(skipZoom = false) { async function updateMapMarkers(skipZoom = false) {
if (!isLoaded.value || !map.value || isUpdatingMarkers.value) return; if (!isLoaded.value || !map.value || isUpdatingMarkers.value) return;
isUpdatingMarkers.value = true;
const currentRequestRouteId = routeStore.selectedRouteId; const currentRequestRouteId = routeStore.selectedRouteId;
const stops = [...routeStore.selectedRouteStops]; if (!currentRequestRouteId) {
try {
if (!currentRequestRouteId || stops.length === 0) {
clearMapMarkers(); clearMapMarkers();
return; return;
} }
isUpdatingMarkers.value = true;
try {
const selectedRouteObj = routeStore.allRoutes.find(r => r.id === currentRequestRouteId) || { id: currentRequestRouteId, short_name: currentRequestRouteId }; const selectedRouteObj = routeStore.allRoutes.find(r => r.id === currentRequestRouteId) || { id: currentRequestRouteId, short_name: currentRequestRouteId };
// ── PASO ATÓMICO: Procesar con carga paralela de GPS y Paradas ──
// Pasamos [] a paradasExistentes para forzar que procesarSeleccionDeRuta maneje el Promise.all
await procesarSeleccionDeRuta( await procesarSeleccionDeRuta(
selectedRouteObj, selectedRouteObj,
stops as BusStop[], routeStore.selectedRouteStops, // El composable decidirá si necesita recargar en paralelo
map.value, map.value,
addCleanMarker, addCleanMarker,
skipZoom, skipZoom,
@ -326,6 +328,7 @@ function selectRouteAndClose(route: any) {
highlightOptimalStopForRoute(); highlightOptimalStopForRoute();
return; return;
} }
showUberSearch.value = false; showUberSearch.value = false;
routeStore.wasSelectedFromMap = true; routeStore.wasSelectedFromMap = true;
routeStore.selectRoute(route.id, route.name); routeStore.selectRoute(route.id, route.name);
@ -476,8 +479,8 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
<div class="split-view"> <div class="split-view">
<div class="map-side"> <div class="map-side">
<div class="map-view"> <div class="map-view">
<div v-if="estasCargandoRuta || errorRuta" class="status-indicator"> <div v-if="flujoCargando || estasCargandoRuta || errorRuta" class="status-indicator">
<div v-if="estasCargandoRuta" class="loading-pill">{{ t('map.calculatingRoute') }}</div> <div v-if="flujoCargando || estasCargandoRuta" class="loading-pill">{{ t('map.calculatingRoute') }}</div>
<div v-if="errorRuta" class="error-pill">{{ errorRuta }}</div> <div v-if="errorRuta" class="error-pill">{{ errorRuta }}</div>
</div> </div>