/** * EPG Cache Service * Caches EPG data locally to reduce API calls */ import type { EPGProgram, EPGCacheEntry } from '../types'; /** * EPG Cache Storage Interface * Implementations: localStorage (web), AsyncStorage (mobile) */ export interface EPGCacheStorage { save(entry: EPGCacheEntry): Promise | void; load(channelId: string): Promise | EPGCacheEntry | null; loadAll(): Promise | EPGCacheEntry[]; clear(): Promise | void; remove(channelId: string): Promise | void; } /** * EPG Cache Service */ export class EPGCacheService { private storage: EPGCacheStorage; private cacheTTL: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds constructor(storage: EPGCacheStorage, cacheTTL?: number) { this.storage = storage; if (cacheTTL !== undefined) { this.cacheTTL = cacheTTL; } } /** * Get cached EPG for a channel */ async get(channelId: string): Promise { const entry = await this.storage.load(channelId); if (!entry) { return null; } // Check if cache is expired const expiresAt = new Date(entry.expiresAt).getTime(); if (Date.now() > expiresAt) { await this.storage.remove(channelId); return null; } return entry.programs; } /** * Cache EPG data for a channel */ async set(channelId: string, programs: EPGProgram[]): Promise { const now = new Date(); const expiresAt = new Date(now.getTime() + this.cacheTTL); const entry: EPGCacheEntry = { channelId, programs, fetchedAt: now.toISOString(), expiresAt: expiresAt.toISOString(), }; await this.storage.save(entry); } /** * Batch cache EPG data for multiple channels */ async setBatch(channelEPGMap: Map): Promise { const promises: Promise[] = []; for (const [channelId, programs] of channelEPGMap.entries()) { promises.push(this.set(channelId, programs)); } await Promise.all(promises); } /** * Clear expired cache entries */ async clearExpired(): Promise { const allEntries = await this.storage.loadAll(); const now = Date.now(); const promises: Promise[] = []; for (const entry of allEntries) { const expiresAt = new Date(entry.expiresAt).getTime(); if (now > expiresAt) { const removeResult = this.storage.remove(entry.channelId); if (removeResult instanceof Promise) { promises.push(removeResult); } } } await Promise.all(promises); } /** * Get all cached channel IDs */ async getCachedChannelIds(): Promise { const allEntries = await this.storage.loadAll(); return allEntries.map((entry) => entry.channelId); } /** * Check if channel has valid cached data */ async hasValidCache(channelId: string): Promise { const entry = await this.storage.load(channelId); if (!entry) { return false; } const expiresAt = new Date(entry.expiresAt).getTime(); return Date.now() <= expiresAt; } /** * Clear all cache */ async clear(): Promise { const clearResult = this.storage.clear(); if (clearResult instanceof Promise) { await clearResult; } } /** * Remove cache for a specific channel */ async remove(channelId: string): Promise { const removeResult = this.storage.remove(channelId); if (removeResult instanceof Promise) { await removeResult; } } } /** * LocalStorage implementation for web */ export class LocalStorageEPGCache implements EPGCacheStorage { private keyPrefix = 'visioo_epg_'; private getKey(channelId: string): string { return `${this.keyPrefix}${channelId}`; } save(entry: EPGCacheEntry): void { try { const key = this.getKey(entry.channelId); localStorage.setItem(key, JSON.stringify(entry)); } catch (error) { console.error('Failed to save EPG cache:', error); } } load(channelId: string): EPGCacheEntry | null { try { const key = this.getKey(channelId); const data = localStorage.getItem(key); return data ? JSON.parse(data) : null; } catch (error) { console.error('Failed to load EPG cache:', error); return null; } } loadAll(): EPGCacheEntry[] { const entries: EPGCacheEntry[] = []; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(this.keyPrefix)) { const data = localStorage.getItem(key); if (data) { try { entries.push(JSON.parse(data)); } catch { // Skip invalid entries } } } } } catch (error) { console.error('Failed to load all EPG cache:', error); } return entries; } clear(): void { try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(this.keyPrefix)) { keysToRemove.push(key); } } keysToRemove.forEach((key) => localStorage.removeItem(key)); } catch (error) { console.error('Failed to clear EPG cache:', error); } } remove(channelId: string): void { try { const key = this.getKey(channelId); localStorage.removeItem(key); } catch (error) { console.error('Failed to remove EPG cache:', error); } } }