Missing files from commit
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled

This commit is contained in:
Antoni Nuñez Romeu
2026-07-07 17:44:36 +02:00
parent 7eb791f181
commit a58ce306bf
10 changed files with 497 additions and 126 deletions
+153 -8
View File
@@ -10,7 +10,87 @@ export function haversineKm(lat1, lon1, lat2, lon2) {
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
export function getUserPosition() {
// --- Position Cache (localStorage) ---
const POSITION_CACHE_KEY = 'farmafinder_last_position';
const POSITION_CACHE_MAX_AGE = 5 * 60 * 1000; // 5 minutes
function getCachedPosition() {
try {
const raw = localStorage.getItem(POSITION_CACHE_KEY);
if (!raw) return null;
const cached = JSON.parse(raw);
if (Date.now() - cached.timestamp > POSITION_CACHE_MAX_AGE) return null;
return { lat: cached.lat, lon: cached.lon };
} catch {
return null;
}
}
function setCachedPosition(lat, lon) {
try {
localStorage.setItem(POSITION_CACHE_KEY, JSON.stringify({ lat, lon, timestamp: Date.now() }));
} catch {}
}
function clearCachedPosition() {
try {
localStorage.removeItem(POSITION_CACHE_KEY);
} catch {}
}
// --- Position Manager ---
let pendingPositionRequest = null;
/**
* Get user position with caching strategy:
* 1. Return cached position if fresh (< 5 min)
* 2. Request browser geolocation in background
* 3. If browser fails, fall back to cached (up to 15 min)
*
* This makes the first request nearly instant for repeat users.
*/
export function getUserPosition({ timeout = 15000, maximumAge = 300000, retries = 1 } = {}) {
// Cancel any pending request before starting a new one
if (pendingPositionRequest) {
pendingPositionRequest.cancel();
pendingPositionRequest = null;
}
// 1. Fast path: return fresh cached position immediately
const cached = getCachedPosition();
if (cached) {
// Also refresh in background (don't await)
refreshPositionInBackground(timeout, maximumAge);
return Promise.resolve(cached);
}
// 2. Slow path: request from browser
return requestBrowserPosition({ timeout, maximumAge, retries }).then(pos => {
setCachedPosition(pos.lat, pos.lon);
return pos;
});
}
/**
* Refresh position silently in background — updates cache for next call.
*/
function refreshPositionInBackground(timeout, maximumAge) {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
pos => {
const newPos = { lat: pos.coords.latitude, lon: pos.coords.longitude };
setCachedPosition(newPos.lat, newPos.lon);
},
() => {}, // silently fail — we already have a cached position
{ timeout, maximumAge, enableHighAccuracy: false }
);
}
/**
* Core browser geolocation request with retry logic.
*/
function requestBrowserPosition({ timeout = 15000, maximumAge = 300000, retries = 1 } = {}) {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocalización no compatible con este navegador'));
@@ -20,17 +100,82 @@ export function getUserPosition() {
reject(new Error('La geolocalización requiere HTTPS (o localhost)'));
return;
}
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
err => {
console.error('[geo] getCurrentPosition failed — code:', err && err.code, 'message:', err && err.message);
let attempts = 0;
let watchId = null;
let settled = false;
function cleanup() {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
}
if (pendingPositionRequest?.id === requestId) {
pendingPositionRequest = null;
}
}
function succeed(pos) {
if (settled) return;
settled = true;
cleanup();
const newPos = { lat: pos.coords.latitude, lon: pos.coords.longitude };
setCachedPosition(newPos.lat, newPos.lon);
resolve(newPos);
}
function fail(err) {
if (settled) return;
attempts++;
if (attempts <= retries && err?.code === 3) {
const delay = Math.min(1000 * Math.pow(2, attempts - 1), 5000);
console.warn(`[geo] timeout, retrying in ${delay}ms (attempt ${attempts}/${retries})`);
setTimeout(() => {
if (!settled) startWatching();
}, delay);
} else {
settled = true;
cleanup();
console.error('[geo] getCurrentPosition failed — code:', err?.code, 'message:', err?.message);
reject(err);
},
{ timeout: 15000, maximumAge: 60000, enableHighAccuracy: false }
);
}
}
function startWatching() {
if (settled) return;
watchId = navigator.geolocation.watchPosition(succeed, fail, {
timeout,
maximumAge,
enableHighAccuracy: false,
});
setTimeout(() => {
if (!settled && watchId !== null) {
fail({ code: 3, message: 'Timeout' });
}
}, timeout + 2000);
}
const requestId = Date.now();
pendingPositionRequest = { id: requestId, cancel: () => { settled = true; cleanup(); } };
startWatching();
});
}
/**
* Clear cached position (e.g. on logout or when user explicitly wants fresh location).
*/
export function clearPositionCache() {
clearCachedPosition();
}
/**
* Check if we have any cached position (for UI hints).
*/
export function hasCachedPosition() {
return getCachedPosition() !== null;
}
export function formatDistance(km) {
if (km < 1) return `${Math.round(km * 1000)} m`;
if (km < 10) return `${km.toFixed(1)} km`;