Skip to main content

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:
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

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
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:
PropertyPurposeChange Trigger
now_playing.idUnique playback instanceSong change detection
now_playing.song.idUnique song identifierAlternative ID check
live.is_liveLive streaming statusLive state change

Performance Benefits

Reduced Network Load

// 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

Client Performance

  1. Faster UI updates: Cached data returns instantly
  2. Fewer HTTP requests: Less network overhead
  3. Battery savings: Reduced network activity on mobile
  4. 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)

Debug Information

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);

Build docs developers (and LLMs) love