Overview
The Cache Manager is a simple yet effective system designed to minimize API calls while ensuring timely updates when song or live status changes. It operates independently of browser cache and provides intelligent change detection.
Architecture
CacheManager Class
Located in js/cache-manager.js, the CacheManager is a lightweight class with minimal dependencies:
class CacheManager {
laurbanData = null ; // Cached API response
lastCheck = 0 ; // Timestamp of last update
currentSongId = null ; // Current song identifier
currentLiveState = false ; // Live stream status
updateInterval = 15000 ; // 15 seconds between updates
}
Initialization
The cache manager is initialized early in the page lifecycle:
<!-- Logger loaded first for dependency -->
< script src = "js/logger.js" ></ script >
<!-- CacheManager loaded after logger -->
< script src = "js/cache-manager.js" ></ script >
// In js/index.js
const cacheManager = new CacheManager ();
Core Functionality
Update Detection
The cache manager determines when fresh data is needed:
Time-Based
Change Detection
needsUpdate () {
const now = Date . now ();
return ! this . laurbanData ||
( now - this . lastCheck ) >= this . updateInterval ;
}
Returns true if:
No data cached yet (laurbanData is null)
More than 15 seconds since last check
hasChanged ( newData ) {
if ( ! this . laurbanData || ! newData ) return true ;
let songChanged = false ;
let liveStateChanged = false ;
// Check song ID change
if ( this . laurbanData . now_playing ?. id !== newData . now_playing ?. id ) {
songChanged = true ;
logger . info ( '🎵 Detectado cambio de canción' );
}
// Check live status change
if ( this . laurbanData . live ?. is_live !== newData . live ?. is_live ) {
liveStateChanged = true ;
logger . info ( '🎥 Detectado cambio en estado de live' );
}
return songChanged || liveStateChanged ;
}
Detects meaningful changes:
✅ Song ID changes (new track)
✅ Live status changes (stream went live/offline)
❌ Ignores minor metadata updates
Data Storage
Simple update and retrieval methods:
// Update cache with new data
update ( newData ) {
this . laurbanData = newData ;
this . lastCheck = Date . now ();
this . currentSongId = newData . now_playing ?. id ;
this . currentLiveState = newData . live ?. is_live || false ;
}
// Retrieve cached data
get () {
return this . laurbanData ;
}
Automatic Timestamps : The update() method automatically records when data was cached using Date.now().
Integration with API Polling
Efficient Update Loop
The main application uses the cache manager to minimize unnecessary API calls:
async function updateSongInfo () {
try {
// Check if update is needed (15 second throttle)
if ( ! cacheManager . needsUpdate ()) {
logger . dev ( 'Using cached data' );
const cachedData = cacheManager . get ();
if ( cachedData ) {
displaySongInfo ( cachedData );
return ;
}
}
// Fetch fresh data from API
const response = await fetch ( CONFIG . API_URL );
const data = await response . json ();
// Check if data actually changed
if ( cacheManager . hasChanged ( data )) {
logger . info ( 'Data changed, updating display' );
cacheManager . update ( data );
displaySongInfo ( data );
// Trigger dependent updates (lyrics, cover, etc.)
handleSongChange ( data );
} else {
logger . dev ( 'No significant changes detected' );
cacheManager . update ( data ); // Update timestamp only
}
} catch ( error ) {
logger . error ( 'Failed to fetch song info:' , error );
// Fallback to cached data if available
const cached = cacheManager . get ();
if ( cached ) {
logger . warn ( 'Using stale cached data' );
displaySongInfo ( cached );
}
}
}
// Poll every 5 seconds
setInterval ( updateSongInfo , CONFIG . UPDATE_INTERVAL ); // 5000ms
API Call Reduction Example
Without cache manager :
Poll interval: 5 seconds
API calls per minute: 12
API calls per hour: 720
With cache manager :
Poll interval: 5 seconds
Cache validity: 15 seconds
Effective API calls: Every 3rd check
API calls per minute: 4
API calls per hour: 240
Result : 67% reduction in API calls
Change Detection Logic
Song Change Workflow
When a song change is detected, the cache manager triggers a cascade of updates:
function handleSongChange ( data ) {
const currentSongId = data ?. now_playing ?. song ?. id ;
if ( currentSongId && currentSongId !== state . lastSongId ) {
logger . success ( '🎵 Nueva canción detectada' );
// 1. Clear old lyrics
if ( state . lyricsManager ) {
state . lyricsManager . clear ();
}
// 2. Update cover art
updateCoverArt ( data . now_playing . song . art );
// 3. Search for new lyrics
const artist = data . now_playing . song . artist ;
const title = data . now_playing . song . title ;
const duration = data . now_playing . duration ;
const elapsed = data . now_playing . elapsed || 0 ;
fetchAndLoadLyrics ( artist , title , duration , elapsed , true );
// 4. Update browser title
document . title = ` ${ artist } - ${ title } | La Urban` ;
// 5. Update Media Session
updateMediaSession ( data );
state . lastSongId = currentSongId ;
}
}
Live Status Detection
The cache manager also tracks when the station goes live:
if ( data . live ?. is_live && ! state . isKickLive ) {
logger . info ( '🔴 Stream went LIVE' );
state . isKickLive = true ;
showLiveIndicator ();
} else if ( ! data . live ?. is_live && state . isKickLive ) {
logger . info ( '⚫ Stream went OFFLINE' );
state . isKickLive = false ;
hideLiveIndicator ();
}
Data Structure
Expected API Response
The cache manager expects data in this format from Azura:
{
"station" : {
"name" : "La Urban" ,
"listen_url" : "https://azura.laurban.cl/listen/laurban/media"
},
"now_playing" : {
"id" : 12345 ,
"song" : {
"id" : "67890" ,
"artist" : "Bad Bunny" ,
"title" : "Monaco" ,
"album" : "Nadie Sabe Lo Que Va a Pasar Mañana" ,
"art" : "https://azura.laurban.cl/api/station/1/art/67890"
},
"elapsed" : 45 ,
"duration" : 223
},
"live" : {
"is_live" : false ,
"streamer_name" : ""
},
"playing_next" : {
"song" : {
"artist" : "Karol G" ,
"title" : "TQG"
}
}
}
Accessed Properties
The cache manager specifically monitors:
Property Purpose Change Trigger now_playing.idUnique playback instance Song change detection now_playing.song.idUnique song identifier Alternative ID check live.is_liveLive streaming status Live state change
Reduced Network Load
Network Impact
Server Impact
// Average API response size: 2-3 KB
// Without caching: 12 calls/min × 3 KB = 36 KB/min
// With caching: 4 calls/min × 3 KB = 12 KB/min
// Bandwidth saved per hour: ~1.4 MB
// Bandwidth saved per day: ~34 MB per user
With 100 concurrent listeners:
Without cache : 72,000 requests/hour
With cache : 24,000 requests/hour
Load reduction : 66%
Faster UI updates : Cached data returns instantly
Fewer HTTP requests : Less network overhead
Battery savings : Reduced network activity on mobile
Offline resilience : Graceful degradation with stale cache
Error Handling
Graceful Fallback
The cache manager provides resilience during network issues:
try {
const response = await fetch ( CONFIG . API_URL );
const data = await response . json ();
cacheManager . update ( data );
displaySongInfo ( data );
} catch ( error ) {
logger . error ( 'API fetch failed:' , error );
// Use cached data as fallback
const cached = cacheManager . get ();
if ( cached ) {
logger . warn ( '⚠️ Using stale cache due to network error' );
displaySongInfo ( cached );
} else {
logger . error ( '❌ No cached data available' );
displayErrorMessage ( 'No se puede conectar al servidor' );
}
}
Stale Data Awareness : When using cached data during errors, the UI should indicate that information may be outdated.
Configuration
Adjustable Parameters
class CacheManager {
// Increase for less frequent API calls
// Decrease for more real-time updates
updateInterval = 15000 ; // milliseconds
}
Considerations when changing interval :
Shorter (< 10s) : More real-time, higher load
15s (default) : Balanced performance
Longer (> 30s) : Better performance, slower updates
Relationship with Poll Interval
// In main application
const CONFIG = {
UPDATE_INTERVAL: 5000 , // Poll every 5 seconds
};
// Cache manager
updateInterval = 15000 ; // Refresh every 15 seconds
// Result: API called every 3rd poll (5s × 3 = 15s)
Monitoring Cache State
Access cache information from browser console:
// Check if cache needs update
cacheManager . needsUpdate ()
// View cached data
cacheManager . get ()
// View last check time
new Date ( cacheManager . lastCheck )
// Time until next update needed
const timeLeft = cacheManager . updateInterval - ( Date . now () - cacheManager . lastCheck );
console . log ( `Next update in: ${ timeLeft } ms` );
Code Example: Complete Flow
// Initialize cache manager
const cacheManager = new CacheManager ();
// Update function with caching
async function updateSongInfo () {
// Step 1: Check cache validity
if ( ! cacheManager . needsUpdate ()) {
const cached = cacheManager . get ();
if ( cached ) {
displaySongInfo ( cached );
return ;
}
}
// Step 2: Fetch fresh data
try {
const response = await fetch ( CONFIG . API_URL );
const data = await response . json ();
// Step 3: Detect changes
if ( cacheManager . hasChanged ( data )) {
// Step 4: Update cache and trigger actions
cacheManager . update ( data );
displaySongInfo ( data );
handleSongChange ( data );
} else {
// Step 5: Update timestamp only
cacheManager . update ( data );
}
} catch ( error ) {
logger . error ( 'Fetch failed:' , error );
// Step 6: Fallback to cache
const cached = cacheManager . get ();
if ( cached ) {
displaySongInfo ( cached );
}
}
}
// Poll every 5 seconds
setInterval ( updateSongInfo , 5000 );