diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js index 55dfe1d..2d7adb1 100644 --- a/src/services/sncfApi.js +++ b/src/services/sncfApi.js @@ -20,6 +20,20 @@ let siriLiteCache = { updates: new Map() }; +// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage +async function fetchWithTimeout(url, options = {}, timeoutMs = 7000) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(id); + return res; + } catch (err) { + clearTimeout(id); + throw err; + } +} + function cleanNum(n) { if (!n) return ''; const m = String(n).match(/\d+/); @@ -39,8 +53,8 @@ async function fetchSiriLiteData() { const updatesMap = new Map(); try { - const res = await fetch(SNCF_SIRI_LITE_TIMETABLE); - if (res.ok) { + const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {}, 8000); + if (res && res.ok) { const xml = await res.text(); const journeys = xml.split(''); @@ -123,7 +137,7 @@ async function fetchSiriLiteData() { updates: updatesMap }; } catch (err) { - console.warn('[sncfApi] Notice: SIRI-Lite refresh error:', err); + console.warn('[sncfApi] Notice: SIRI-Lite refresh error/timeout:', err); } return siriLiteCache; @@ -143,12 +157,12 @@ async function fetchGtfsRealtimeData() { try { const [rtRes, alertsRes] = await Promise.allSettled([ - fetch(SNCF_GTFS_RT_TRIP_UPDATES), - fetch(SNCF_GTFS_RT_SERVICE_ALERTS) + fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 6000), + fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 6000) ]); // 1. Décodage Trip Updates (Retards, Suppressions) - if (rtRes.status === 'fulfilled' && rtRes.value.ok) { + if (rtRes.status === 'fulfilled' && rtRes.value && rtRes.value.ok) { const buffer = await rtRes.value.arrayBuffer(); const feed = transit_realtime.FeedMessage.decode(new Uint8Array(buffer)); @@ -188,7 +202,7 @@ async function fetchGtfsRealtimeData() { } // 2. Décodage Service Alerts (Causes de retards & perturbations) - if (alertsRes.status === 'fulfilled' && alertsRes.value.ok) { + if (alertsRes.status === 'fulfilled' && alertsRes.value && alertsRes.value.ok) { const buffer = await alertsRes.value.arrayBuffer(); const alertFeed = transit_realtime.FeedMessage.decode(new Uint8Array(buffer)); @@ -294,13 +308,29 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { if (!uic) return null; try { + const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : ''; + const stationUrl = `${publicUrl}/data/stations/${uic}.json`; + + // Chargement parallèle tolérant aux pannes réseau / CORS const [jsonRes, rtData, siriData] = await Promise.all([ - fetch(`/data/stations/${uic}.json`), - fetchGtfsRealtimeData(), - fetchSiriLiteData() + fetch(stationUrl).catch(err => { + console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err); + return null; + }), + fetchGtfsRealtimeData().catch(err => { + console.warn('[sncfApi] Notice: GTFS-RT error:', err); + return { updates: new Map(), alerts: [] }; + }), + fetchSiriLiteData().catch(err => { + console.warn('[sncfApi] Notice: SIRI-Lite error:', err); + return { platforms: new Map(), updates: new Map() }; + }) ]); - if (!jsonRes.ok) return null; + if (!jsonRes || !jsonRes.ok) { + console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl} (statut : ${jsonRes ? jsonRes.status : 'réseau bloqué'})`); + return null; + } const data = await jsonRes.json(); const rawList = mode === 'arrivees' ? data.arrivees : data.departs; @@ -311,19 +341,19 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi const currentYYYYMMDD = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; - // Filtrage complet de circulation GTFS - const circulatingList = rawList.filter(t => { - // 1. Si la date courante figure dans les dates explicites + // 1. Filtrage précis par date GTFS (si la date du jour est dans la plage de validité) + let circulatingList = rawList.filter(t => { + // Si la date courante figure dans les dates explicites if (Array.isArray(t.dates) && t.dates.length > 0 && t.dates.includes(currentYYYYMMDD)) { return true; } - // 2. Si le jour de la semaine est actif dans la période de validité + // Si le jour de la semaine est actif dans la période de validité if (Array.isArray(t.days) && t.days.length > 0 && t.days.includes(currentDay)) { if ((!t.start || currentYYYYMMDD >= t.start) && (!t.end || currentYYYYMMDD <= t.end)) { return true; } } - // 3. Train de nuit parti la veille et arrivant aujourd'hui (heures >= 24h) + // Train de nuit parti la veille et arrivant aujourd'hui (heures >= 24h) if (Array.isArray(t.days) && t.days.length > 0) { const prevDay = (currentDay + 6) % 7; const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr); @@ -333,13 +363,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { return true; } } - // 4. Si aucune règle n'est renseignée + // Si aucune règle n'est renseignée if (!t.dates?.length && !t.days?.length) return true; return false; }); + // 2. Fallback robuste : si la date actuelle est hors calendrier GTFS, filtrer par jour de la semaine if (circulatingList.length === 0) { - return []; + circulatingList = rawList.filter(t => { + if (Array.isArray(t.days) && t.days.length > 0) { + return t.days.includes(currentDay); + } + return true; + }); + } + + // 3. Fallback complet : si toujours vide, renvoyer la grille quotidienne complète + if (circulatingList.length === 0) { + circulatingList = rawList; } // Helper de résolution ultra-robuste de quai par couple (train, gare)