fix(map): two-layer cancel token system to fully prevent orphan markers/polylines on banner close

This commit is contained in:
2026-03-04 12:04:02 -05:00
parent 6c197ba1f8
commit c5e5946738
2 changed files with 53 additions and 19 deletions

View File

@ -31,7 +31,8 @@ export const useFlujoPrincipal = () => {
map: google.maps.Map | undefined, map: google.maps.Map | undefined,
addCleanMarker: Function, addCleanMarker: Function,
skipGuidedZoom = false, skipGuidedZoom = false,
onStopClick?: (stop: BusStop) => void onStopClick?: (stop: BusStop) => void,
cancelToken?: { cancelled: boolean } // token de cancelación pasado desde el llamador
) => { ) => {
if (!map) return if (!map) return
@ -50,6 +51,13 @@ export const useFlujoPrincipal = () => {
paradasExistentes.length > 0 ? Promise.resolve(paradasExistentes) : routeStore.loadRouteStops(_ruta.id) 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 ── // ── PASO 3: Asignación Segura ──
let ubicacion: { lat: number, lng: number } | null = null; let ubicacion: { lat: number, lng: number } | null = null;
if (ubicacionRes.status === 'fulfilled') { if (ubicacionRes.status === 'fulfilled') {
@ -58,12 +66,6 @@ export const useFlujoPrincipal = () => {
console.warn('SIBU | GPS falló o fue denegado'); 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[] = []; let paradas: BusStop[] = [];
if (paradasRes.status === 'fulfilled') { if (paradasRes.status === 'fulfilled') {
// Si loadRouteStops no devolviera los datos directamente, los tomamos del store // 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 // Si no detectamos GPS, trazamos la ruta completa sin zoom guiado
if (!ubicacion) { if (!ubicacion) {
await trazarRuta(paradasFormateadas, map, false); await trazarRuta(paradasFormateadas, map, false);
if (cancelToken?.cancelled) { limpiarMapa(); return; }
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)))
map.fitBounds(bounds, { top: 100, bottom: 100, left: 60, right: 60 }) map.fitBounds(bounds, { top: 100, bottom: 100, left: 60, right: 60 })
@ -93,11 +96,19 @@ export const useFlujoPrincipal = () => {
} }
// ── PASO 4: Dibujar y Renderizar ── // ── PASO 4: Dibujar y Renderizar ──
if (routeStore.selectedRouteId !== _ruta.id) return; if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) {
limpiarMapa();
return;
}
// Dibujar ruta completa (fondo) // Dibujar ruta completa (fondo)
await trazarRuta(paradasFormateadas, map, true); await trazarRuta(paradasFormateadas, map, true);
if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) {
limpiarMapa(); // limpiar polylines ya dibujadas
return;
}
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)))
@ -108,7 +119,10 @@ export const useFlujoPrincipal = () => {
await encontrarParadaCercana(ubicacion, paradas, map) await encontrarParadaCercana(ubicacion, paradas, map)
const paradaCercanaFound = paradaCercana.value const paradaCercanaFound = paradaCercana.value
if (routeStore.selectedRouteId !== _ruta.id) return; if (cancelToken?.cancelled || routeStore.selectedRouteId !== _ruta.id) {
limpiarMapa();
return;
}
if (!paradaCercanaFound) return; if (!paradaCercanaFound) return;
// Dibujar tramo relevante // 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 // Renderizado Condicional de Marcadores
paradasFormateadas.forEach((p, i) => { paradasFormateadas.forEach((p, i) => {
const esCercana = i === idx; const esCercana = i === idx;

View File

@ -49,6 +49,8 @@ const userMarker = shallowRef<any>(null);
const isUpdatingMarkers = ref(false); const isUpdatingMarkers = ref(false);
// Cancellation token: increment to invalidate any in-flight marker draw // Cancellation token: increment to invalidate any in-flight marker draw
const markerGenerationId = ref(0); 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<Map<string, any>>(new Map()); const unitMarkers = shallowRef<Map<string, any>>(new Map());
const unitFetchInterval = ref<any>(null); const unitFetchInterval = ref<any>(null);
const userCoords = ref<{ lat: number; lng: number } | null>(null); const userCoords = ref<{ lat: number; lng: number } | null>(null);
@ -101,13 +103,17 @@ function closeUberSearch() {
async function animateAndReload() { async function animateAndReload() {
isBannerClosing.value = true; 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++; 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); routeStore.setWasSelectedFromMap(false);
clearMapMarkers(); // Limpiar mapa INMEDIATAMENTE - dos capas de limpieza
clearMapMarkers(); // limpia markers[] y globalOverlays
limpiarTodoCentralizado(); // limpia polylines[], infoWindows[], circles[]
limpiarCaminata(); limpiarCaminata();
routeStore.clearSelection(); routeStore.clearSelection();
@ -116,10 +122,10 @@ async function animateAndReload() {
showETACard.value = false; showETACard.value = false;
routePhase.value = 'idle'; routePhase.value = 'idle';
// Limpieza extra garantizada después de un tick, por si algún await en vuelo // Segunda pasada tras el tick: por si algún await en vuelo terminó justo antes
// terminó justo antes e intentó redibujar markers
await nextTick(); await nextTick();
clearMapMarkers(); clearMapMarkers();
limpiarTodoCentralizado();
if (map.value) { if (map.value) {
setCenter(mapStore.center.lat, mapStore.center.lng); 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 // Capturar el token de generación ANTES de cualquier await
const myGeneration = markerGenerationId.value; 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; isUpdatingMarkers.value = true;
try { 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 };
// Guard de generación: si se canceló mientras esperábamos, abortar // Guard de generación: si se canceló mientras esperabamos, abortar
if (markerGenerationId.value !== myGeneration) return; if (myToken.cancelled || markerGenerationId.value !== myGeneration) {
limpiarTodoCentralizado();
return;
}
await procesarSeleccionDeRuta( await procesarSeleccionDeRuta(
selectedRouteObj, selectedRouteObj,
@ -320,16 +332,18 @@ async function updateMapMarkers(skipZoom = false) {
skipZoom, skipZoom,
(stop: BusStop) => { (stop: BusStop) => {
// Solo actualizar si aún somos la generación vigente // Solo actualizar si aún somos la generación vigente
if (markerGenerationId.value === myGeneration) { if (!myToken.cancelled && markerGenerationId.value === myGeneration) {
paradaCercana.value = stop; paradaCercana.value = stop;
showETACard.value = true; showETACard.value = true;
} }
} },
myToken // <-- pasar el cancel token al composable
); );
// Guard final: verificar que no se canceló durante el await largo // 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(); clearMapMarkers();
limpiarTodoCentralizado();
return; return;
} }