From 6c197ba1f85ec309d0b288aef248c18216e1a379 Mon Sep 17 00:00:00 2001 From: Hanzo_dev <2002samudiojohan@gmail.com> Date: Wed, 4 Mar 2026 11:49:52 -0500 Subject: [PATCH] fix(map): add generation token to hard-cancel in-flight marker draws on banner close --- frontend/src/views/MapView.vue | 45 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index 1600230..e98d678 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -47,6 +47,8 @@ const routePhase = ref<'idle' | 'eta' | 'navigating'>('idle'); const promoMarkers = shallowRef([]); const userMarker = shallowRef(null); const isUpdatingMarkers = ref(false); +// Cancellation token: increment to invalidate any in-flight marker draw +const markerGenerationId = ref(0); const unitMarkers = shallowRef>(new Map()); const unitFetchInterval = ref(null); const userCoords = ref<{ lat: number; lng: number } | null>(null); @@ -99,7 +101,10 @@ function closeUberSearch() { async function animateAndReload() { isBannerClosing.value = true; - // 🔥 CRÍTICO + // 🔥 CRÍTICO: Invalidar cualquier dibujado de markers en vuelo + markerGenerationId.value++; + isUpdatingMarkers.value = false; // liberar el lock para que no quede bloqueado + routeStore.setWasSelectedFromMap(false); clearMapMarkers(); @@ -111,8 +116,11 @@ async function animateAndReload() { showETACard.value = false; routePhase.value = 'idle'; - // No es necesario recargar TODO el objeto del mapa, consume cuota innecesariamente. - // Limpiamos y reseteamos el estado visual + // Limpieza extra garantizada después de un tick, por si algún await en vuelo + // terminó justo antes e intentó redibujar markers + await nextTick(); + clearMapMarkers(); + if (map.value) { setCenter(mapStore.center.lat, mapStore.center.lng); setZoom(mapStore.zoom); @@ -293,33 +301,48 @@ async function updateMapMarkers(skipZoom = false) { return; } + // Capturar el token de generación ANTES de cualquier await + const myGeneration = markerGenerationId.value; + isUpdatingMarkers.value = true; try { 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 + // Guard de generación: si se canceló mientras esperábamos, abortar + if (markerGenerationId.value !== myGeneration) return; + await procesarSeleccionDeRuta( selectedRouteObj, - routeStore.selectedRouteStops, // El composable decidirá si necesita recargar en paralelo + routeStore.selectedRouteStops, map.value, addCleanMarker, skipZoom, (stop: BusStop) => { - paradaCercana.value = stop; - showETACard.value = true; + // Solo actualizar si aún somos la generación vigente + if (markerGenerationId.value === myGeneration) { + paradaCercana.value = stop; + showETACard.value = true; + } } ); + + // Guard final: verificar que no se canceló durante el await largo + if (markerGenerationId.value !== myGeneration || routeStore.selectedRouteId !== currentRequestRouteId) { + clearMapMarkers(); + return; + } + reDrawUserMarker(); - if (routeStore.selectedRouteId !== currentRequestRouteId) return; - if (routeStore.wasSelectedFromMap && !skipZoom) { await highlightOptimalStopForRoute(); } } finally { - isUpdatingMarkers.value = false; + // Solo liberar el lock si somos la generación actual + if (markerGenerationId.value === myGeneration) { + isUpdatingMarkers.value = false; + } } }