fix(map): two-layer cancel token system to fully prevent orphan markers/polylines on banner close
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user