PageSourceSearch

https://wxli.ai/assets/js/wxli-api-cache.js

js wxli.ai collected 2026-10-05 13:20:10 UTC 13,315 bytes, 422 lines download raw bytes

1/**
2 * WXLI API Cache - Sistema centralizado de cache y sincronización
3 * Reduce llamadas duplicadas a la API entre ventanas/pestañas
4 *
5 * @author WXLI Tech
6 * @version 2.0.0
7 */
8
9(function(window) {
10  'use strict';
11
12  // ============================================================================
13  // CONFIGURACIÓN
14  // ============================================================================
15  const CONFIG = {
16    API_BASE: 'https://api.core.wxli.ai',
17    NOWPLAYING_ENDPOINT: '/nowplaying/1',
18    SCHEDULE_ENDPOINT: '/station/1/schedule',
19
20    // TTL dinámico según estado
21    CACHE_TTL_NORMAL: 10000,      // 10 segundos en estado normal
22    CACHE_TTL_TRANSITION: 5000,   // 5 segundos durante transición de canción
23    CACHE_TTL_BACKGROUND: 30000,  // 30 segundos cuando la página está oculta
24
25    // LocalStorage keys para sincronización
26    STORAGE_KEY_NOWPLAYING: 'wxli-api-nowplaying-cache',
27    STORAGE_KEY_SCHEDULE: 'wxli-api-schedule-cache',
28
29    // BroadcastChannel para sincronización en tiempo real
30    BROADCAST_CHANNEL: 'wxli-api-sync'
31  };
32
33  // ============================================================================
34  // GENERADOR DE TOKEN DE SESIÓN
35  // ============================================================================
36  function wxliToken() {
37    const ts = Math.floor(Date.now() / 1000);
38    return btoa('wxli:' + ts);
39  }
40
41  // ============================================================================
42  // CACHE EN MEMORIA
43  // ============================================================================
44  const memoryCache = {
45    nowplaying: {
46      data: null,
47      timestamp: 0,
48      pending: null,
49      songId: null
50    },
51    schedule: {
52      data: null,
53      timestamp: 0,
54      pending: null
55    }
56  };
57
58  // Estado de la aplicación
59  let isInTransition = false;
60  let isPageHidden = false;
61  let broadcastChannel = null;
62
63  // ============================================================================
64  // BROADCAST CHANNEL PARA SINCRONIZACIÓN ENTRE VENTANAS
65  // ============================================================================
66  function initBroadcastChannel() {
67    if ('BroadcastChannel' in window) {
68      try {
69        broadcastChannel = new BroadcastChannel(CONFIG.BROADCAST_CHANNEL);
70
71        broadcastChannel.onmessage = function(event) {
72          const message = event.data;
73
74          if (message.type === 'nowplaying-update' && message.data) {
75            // Otra ventana actualizó los datos, usar su cache
76            const now = Date.now();
77            const messageAge = now - message.timestamp;
78
79            // Solo aceptar si el mensaje es reciente (< 2 segundos)
80            if (messageAge < 2000) {
81              memoryCache.nowplaying.data = message.data;
82              memoryCache.nowplaying.timestamp = message.timestamp;
83              memoryCache.nowplaying.songId = message.songId;
84
85              // Notificar a los listeners locales
86              window.dispatchEvent(new CustomEvent('wxli-nowplaying-updated', {
87                detail: { data: message.data, fromBroadcast: true }
88              }));
89            }
90          } else if (message.type === 'schedule-update' && message.data) {
91            const messageAge = Date.now() - message.timestamp;
92            if (messageAge < 5000) {
93              memoryCache.schedule.data = message.data;
94              memoryCache.schedule.timestamp = message.timestamp;
95            }
96          }
97        };
98      } catch (e) {
99      }
100    }
101  }
102
103  // Broadcast data to other windows
104  function broadcastUpdate(type, data, songId) {
105    if (broadcastChannel) {
106      try {
107        broadcastChannel.postMessage({
108          type: type,
109          data: data,
110          songId: songId,
111          timestamp: Date.now()
112        });
113      } catch (e) {
114        // Silently fail if broadcast fails
115      }
116    }
117  }
118
119  // ============================================================================
120  // LOCALSTORAGE FALLBACK PARA SINCRONIZACIÓN
121  // ============================================================================
122  function saveToLocalStorage(key, data) {
123    try {
124      localStorage.setItem(key, JSON.stringify({
125        data: data,
126        timestamp: Date.now()
127      }));
128    } catch (e) {
129      // Quota exceeded or other error - silently fail
130    }
131  }
132
133  function loadFromLocalStorage(key, maxAge) {
134    try {
135      const stored = localStorage.getItem(key);
136      if (stored) {
137        const parsed = JSON.parse(stored);
138        const age = Date.now() - parsed.timestamp;
139        if (age < maxAge) {
140          return parsed;
141        }
142      }
143    } catch (e) {
144      // Parse error - ignore
145    }
146    return null;
147  }
148
149  // ============================================================================
150  // UTILIDAD: LIMPIEZA DE NOMBRES DE TRACKS
151  // ============================================================================
152  function cleanTrackName(str) {
153    if (!str) return str;
154    return str
155      .replace(/\s{2,}/g, ' ')
156      .trim();
157  }
158
159  function cleanNowPlayingData(data) {
160    if (!data) return data;
161
162    if (data.now_playing && data.now_playing.song) {
163      data.now_playing.song.title = cleanTrackName(data.now_playing.song.title);
164      data.now_playing.song.artist = cleanTrackName(data.now_playing.song.artist);
165    }
166
167    if (Array.isArray(data.song_history)) {
168      data.song_history.forEach(function(item) {
169        if (item.song) {
170          item.song.title = cleanTrackName(item.song.title);
171          item.song.artist = cleanTrackName(item.song.artist);
172        }
173      });
174    }
175
176    return data;
177  }
178
179  // ============================================================================
180  // FUNCIÓN PRINCIPAL: FETCH NOW PLAYING
181  // ============================================================================
182  async function fetchNowPlaying(forceRefresh) {
183    const now = Date.now();
184    const cache = memoryCache.nowplaying;
185
186    // Determinar TTL basado en estado
187    let ttl = CONFIG.CACHE_TTL_NORMAL;
188    if (isInTransition) {
189      ttl = CONFIG.CACHE_TTL_TRANSITION;
190    } else if (isPageHidden) {
191      ttl = CONFIG.CACHE_TTL_BACKGROUND;
192    }
193
194    // Si hay una petición pendiente, esperar a que termine (deduplicación)
195    if (cache.pending) {
196      return cache.pending;
197    }
198
199    // Si el cache es válido y no se fuerza refresh, usar cache
200    if (!forceRefresh && cache.data && (now - cache.timestamp) < ttl) {
201      return cache.data;
202    }
203
204    // Intentar cargar de localStorage (útil para nuevas pestañas)
205    if (!forceRefresh && !cache.data) {
206      const stored = loadFromLocalStorage(CONFIG.STORAGE_KEY_NOWPLAYING, ttl);
207      if (stored) {
208        cache.data = stored.data;
209        cache.timestamp = stored.timestamp;
210        return cache.data;
211      }
212    }
213
214    // Crear nueva petición con jitter para evitar thundering herd
215    const jitter = Math.random() * 500;
216
217    cache.pending = new Promise(function(resolve, reject) {
218      setTimeout(function() {
219        fetch(CONFIG.API_BASE + CONFIG.NOWPLAYING_ENDPOINT, {
220          headers: { 'X-WXLI-Session': wxliToken() },
221          cache: 'no-store'
222        })
223          .then(function(response) {
224            return response.json();
225          })
226          .then(function(data) {
227            const fetchTime = Date.now();
228
229            // Limpiar paréntesis y corchetes de los nombres de tracks
230            cleanNowPlayingData(data);
231
232            // Actualizar cache en memoria
233            cache.data = data;
234            cache.timestamp = fetchTime;
235            cache.pending = null;
236
237            // Extraer songId para detección de cambios
238            const songId = data.now_playing && data.now_playing.song
239              ? (data.now_playing.song.id || data.now_playing.song.title)
240              : null;
241            cache.songId = songId;
242
243            // Guardar en localStorage para otras pestañas
244            saveToLocalStorage(CONFIG.STORAGE_KEY_NOWPLAYING, data);
245
246            // Broadcast a otras ventanas
247            broadcastUpdate('nowplaying-update', data, songId);
248
249            // Notificar listeners locales
250            window.dispatchEvent(new CustomEvent('wxli-nowplaying-updated', {
251              detail: { data: data, fromBroadcast: false }
252            }));
253
254            resolve(data);
255          })
256          .catch(function(error) {
257            cache.pending = null;
258            reject(error);
259          });
260      }, jitter);
261    });
262
263    return cache.pending;
264  }
265
266  // ============================================================================
267  // FUNCIÓN: FETCH SCHEDULE
268  // ============================================================================
269  async function fetchSchedule(startDate, endDate, forceRefresh) {
270    const now = Date.now();
271    const cache = memoryCache.schedule;
272    const ttl = isPageHidden ? CONFIG.CACHE_TTL_BACKGROUND : 60000; // 1 minuto para schedule
273
274    // Deduplicación
275    if (cache.pending) {
276      return cache.pending;
277    }
278
279    // Cache válido
280    if (!forceRefresh && cache.data && (now - cache.timestamp) < ttl) {
281      return cache.data;
282    }
283
284    // Construir URL con parámetros de fecha
285    let url = CONFIG.API_BASE + CONFIG.SCHEDULE_ENDPOINT;
286    if (startDate && endDate) {
287      url += '?start=' + startDate + '&end=' + endDate;
288    }
289
290    cache.pending = fetch(url, {
291      headers: { 'X-WXLI-Session': wxliToken() }
292    })
293      .then(function(response) {
294        return response.json();
295      })
296      .then(function(data) {
297        cache.data = data;
298        cache.timestamp = Date.now();
299        cache.pending = null;
300
301        // Guardar en localStorage
302        saveToLocalStorage(CONFIG.STORAGE_KEY_SCHEDULE, data);
303
304        // Broadcast a otras ventanas
305        broadcastUpdate('schedule-update', data, null);
306
307        return data;
308      })
309      .catch(function(error) {
310        cache.pending = null;
311        throw error;
312      });
313
314    return cache.pending;
315  }
316
317  // ============================================================================
318  // FUNCIONES DE CONTROL DE ESTADO
319  // ============================================================================
320  function setTransitionState(inTransition) {
321    isInTransition = inTransition;
322  }
323
324  function setPageHidden(hidden) {
325    isPageHidden = hidden;
326  }
327
328  function getCacheStats() {
329    return {
330      nowplaying: {
331        hasData: !!memoryCache.nowplaying.data,
332        age: memoryCache.nowplaying.timestamp ? Date.now() - memoryCache.nowplaying.timestamp : null,
333        songId: memoryCache.nowplaying.songId
334      },
335      schedule: {
336        hasData: !!memoryCache.schedule.data,
337        age: memoryCache.schedule.timestamp ? Date.now() - memoryCache.schedule.timestamp : null
338      },
339      isInTransition: isInTransition,
340      isPageHidden: isPageHidden
341    };
342  }
343
344  function invalidateCache(type) {
345    if (type === 'nowplaying' || type === 'all') {
346      memoryCache.nowplaying.data = null;
347      memoryCache.nowplaying.timestamp = 0;
348    }
349    if (type === 'schedule' || type === 'all') {
350      memoryCache.schedule.data = null;
351      memoryCache.schedule.timestamp = 0;
352    }
353  }
354
355  // Obtener datos cacheados sin hacer fetch (útil para UI)
356  function getCachedNowPlaying() {
357    return memoryCache.nowplaying.data;
358  }
359
360  function getCachedSchedule() {
361    return memoryCache.schedule.data;
362  }
363
364  // ============================================================================
365  // VISIBILITY CHANGE HANDLER
366  // ============================================================================
367  document.addEventListener('visibilitychange', function() {
368    setPageHidden(document.hidden);
369
370    if (!document.hidden) {
371      // Página visible de nuevo - forzar refresh si el cache es muy viejo
372      const cache = memoryCache.nowplaying;
373      if (cache.data && (Date.now() - cache.timestamp) > CONFIG.CACHE_TTL_BACKGROUND) {
374        fetchNowPlaying(true).catch(function() {});
375      }
376    }
377  });
378
379  // ============================================================================
380  // INICIALIZACIÓN
381  // ============================================================================
382  initBroadcastChannel();
383
384  // Cerrar BroadcastChannel al descargar la página para liberar recursos
385  window.addEventListener('pagehide', function() {
386    if (broadcastChannel) {
387      broadcastChannel.close();
388      broadcastChannel = null;
389    }
390  }, { capture: true });
391
392  // ============================================================================
393  // EXPORTAR API PÚBLICA
394  // ============================================================================
395  window.WXLICache = {
396    // Funciones principales
397    fetchNowPlaying: fetchNowPlaying,
398    fetchSchedule: fetchSchedule,
399
400    // Token de sesión (para compatibilidad)
401    wxliToken: wxliToken,
402
403    // Control de estado
404    setTransitionState: setTransitionState,
405    setPageHidden: setPageHidden,
406
407    // Utilidades
408    getCacheStats: getCacheStats,
409    invalidateCache: invalidateCache,
410    getCachedNowPlaying: getCachedNowPlaying,
411    getCachedSchedule: getCachedSchedule,
412
413    // Configuración (solo lectura)
414    CONFIG: Object.freeze(CONFIG)
415  };
416
417  // Alias para compatibilidad con código existente
418  window.wxliToken = wxliToken;
419  window.wxliFetchNowPlaying = fetchNowPlaying;
420
421
422})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.