diff --git a/frontend/src/composables/useFlujoPrincipal.ts b/frontend/src/composables/useFlujoPrincipal.ts index c804864..255e370 100644 --- a/frontend/src/composables/useFlujoPrincipal.ts +++ b/frontend/src/composables/useFlujoPrincipal.ts @@ -31,7 +31,8 @@ export const useFlujoPrincipal = () => { map: google.maps.Map | undefined, addCleanMarker: Function, skipGuidedZoom = false, - onStopClick?: (stop: BusStop) => void + onStopClick?: (stop: BusStop) => void, + cancelToken?: { cancelled: boolean } // token de cancelación pasado desde el llamador ) => { if (!map) return @@ -50,6 +51,13 @@ export const useFlujoPrincipal = () => { paradasExistentes.length > 0 ? Promise.resolve(paradasExistentes) : routeStore.loadRouteStops(_ruta.id) ]); + // Guard: abandono si el token fue cancelado o la ruta ya no está activa + if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) { + console.log('SIBU | procesarSeleccionDeRuta abortado tras Promise.allSettled (ruta ya no activa o cancelada)'); + limpiarMapa(); // limpiar cualquier polyline ya dibujada + return; + } + // ── PASO 3: Asignación Segura ── let ubicacion: { lat: number, lng: number } | null = null; if (ubicacionRes.status === 'fulfilled') { @@ -58,12 +66,6 @@ export const useFlujoPrincipal = () => { console.warn('SIBU | GPS falló o fue denegado'); } - // Guard contra race condition: si el usuario cerró el banner mientras cargaba - if (routeStore.selectedRouteId !== _ruta.id) { - console.log('SIBU | Carga abortada: La ruta ya no está seleccionada.'); - return; - } - let paradas: BusStop[] = []; if (paradasRes.status === 'fulfilled') { // Si loadRouteStops no devolviera los datos directamente, los tomamos del store @@ -86,6 +88,7 @@ export const useFlujoPrincipal = () => { // Si no detectamos GPS, trazamos la ruta completa sin zoom guiado if (!ubicacion) { await trazarRuta(paradasFormateadas, map, false); + if (cancelToken?.cancelled) { limpiarMapa(); return; } 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 }) @@ -93,11 +96,19 @@ export const useFlujoPrincipal = () => { } // ── PASO 4: Dibujar y Renderizar ── - if (routeStore.selectedRouteId !== _ruta.id) return; + if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) { + limpiarMapa(); + return; + } // Dibujar ruta completa (fondo) await trazarRuta(paradasFormateadas, map, true); + if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) { + limpiarMapa(); // limpiar polylines ya dibujadas + return; + } + if (skipGuidedZoom) { const bounds = new google.maps.LatLngBounds() paradasFormateadas.forEach(p => bounds.extend(new google.maps.LatLng(p.latitud, p.longitud))) @@ -108,7 +119,10 @@ export const useFlujoPrincipal = () => { await encontrarParadaCercana(ubicacion, paradas, map) const paradaCercanaFound = paradaCercana.value - if (routeStore.selectedRouteId !== _ruta.id) return; + if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) { + limpiarMapa(); + return; + } if (!paradaCercanaFound) return; // Dibujar tramo relevante @@ -125,6 +139,12 @@ export const useFlujoPrincipal = () => { } } + // Guard final antes de pintar markers + if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) { + limpiarMapa(); + return; + } + // Renderizado Condicional de Marcadores paradasFormateadas.forEach((p, i) => { const esCercana = i === idx; diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index e98d678..244290e 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -49,6 +49,8 @@ const userMarker = shallowRef(null); const isUpdatingMarkers = ref(false); // Cancellation token: increment to invalidate any in-flight marker draw const markerGenerationId = ref(0); +// Object-based cancel token passed into procesarSeleccionDeRuta so it can abort from within +let currentCancelToken: { cancelled: boolean } = { cancelled: false }; const unitMarkers = shallowRef>(new Map()); const unitFetchInterval = ref(null); const userCoords = ref<{ lat: number; lng: number } | null>(null); @@ -101,13 +103,17 @@ function closeUberSearch() { async function animateAndReload() { isBannerClosing.value = true; - // 🔥 CRÍTICO: Invalidar cualquier dibujado de markers en vuelo + // 🔥 CANCELACIÓN TOTAL: invalidar la operación en vuelo desde adentro Y desde afuera markerGenerationId.value++; - isUpdatingMarkers.value = false; // liberar el lock para que no quede bloqueado + currentCancelToken.cancelled = true; // aborta procesarSeleccionDeRuta desde adentro + currentCancelToken = { cancelled: false }; // prepara un token limpio para la próxima operación + isUpdatingMarkers.value = false; // liberar el lock routeStore.setWasSelectedFromMap(false); - clearMapMarkers(); + // Limpiar mapa INMEDIATAMENTE - dos capas de limpieza + clearMapMarkers(); // limpia markers[] y globalOverlays + limpiarTodoCentralizado(); // limpia polylines[], infoWindows[], circles[] limpiarCaminata(); routeStore.clearSelection(); @@ -116,10 +122,10 @@ async function animateAndReload() { showETACard.value = false; routePhase.value = 'idle'; - // Limpieza extra garantizada después de un tick, por si algún await en vuelo - // terminó justo antes e intentó redibujar markers + // Segunda pasada tras el tick: por si algún await en vuelo terminó justo antes await nextTick(); clearMapMarkers(); + limpiarTodoCentralizado(); if (map.value) { setCenter(mapStore.center.lat, mapStore.center.lng); @@ -303,14 +309,20 @@ async function updateMapMarkers(skipZoom = false) { // Capturar el token de generación ANTES de cualquier await const myGeneration = markerGenerationId.value; + // Crear un token de cancelación para esta operación específica + const myToken = { cancelled: false }; + currentCancelToken = myToken; isUpdatingMarkers.value = true; try { const selectedRouteObj = routeStore.allRoutes.find(r => r.id === currentRequestRouteId) || { id: currentRequestRouteId, short_name: currentRequestRouteId }; - // Guard de generación: si se canceló mientras esperábamos, abortar - if (markerGenerationId.value !== myGeneration) return; + // Guard de generación: si se canceló mientras esperabamos, abortar + if (myToken.cancelled || markerGenerationId.value !== myGeneration) { + limpiarTodoCentralizado(); + return; + } await procesarSeleccionDeRuta( selectedRouteObj, @@ -320,16 +332,18 @@ async function updateMapMarkers(skipZoom = false) { skipZoom, (stop: BusStop) => { // Solo actualizar si aún somos la generación vigente - if (markerGenerationId.value === myGeneration) { + if (!myToken.cancelled && markerGenerationId.value === myGeneration) { paradaCercana.value = stop; showETACard.value = true; } - } + }, + myToken // <-- pasar el cancel token al composable ); // Guard final: verificar que no se canceló durante el await largo - if (markerGenerationId.value !== myGeneration || routeStore.selectedRouteId !== currentRequestRouteId) { + if (myToken.cancelled || markerGenerationId.value !== myGeneration || routeStore.selectedRouteId !== currentRequestRouteId) { clearMapMarkers(); + limpiarTodoCentralizado(); return; }