/** * Smart Polling Service for Live Match Updates * Implements dynamic frequency polling based on match states and user activity */ class SmartPollingService { constructor(options = {}) { // Get configuration from config system if available const config = window.smartPollingConfig?.getConfig() || {}; this.options = { // Polling intervals (in milliseconds) intervals: config.intervals || { liveMatches: 30000, // 30 seconds when live matches present upcomingMatches: 150000, // 2.5 minutes when matches starting within 2 hours noMatches: 600000, // 10 minutes when no live/upcoming matches backgroundTab: 300000 // 5 minutes when tab is not active }, // Date to poll for (default: today in user's local timezone) date: options.date || (() => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; })(), // Callback when updates are received onUpdate: options.onUpdate || null, // Callback when polling state changes onStateChange: options.onStateChange || null, // Enable debug logging debug: config.debug || options.debug || false, ...options }; this.isActive = false; this.currentInterval = null; this.timeoutId = null; this.isTabVisible = true; this.lastUpdateTime = null; this.consecutiveErrors = 0; this.maxErrors = 5; this.currentPollingMode = 'noMatches'; // Bind methods to maintain context this.handleVisibilityChange = this.handleVisibilityChange.bind(this); this.poll = this.poll.bind(this); // Initialize visibility API this.initializeVisibilityAPI(); // Listen for configuration changes this.listenForConfigChanges(); this.log('Smart Polling Service initialized'); } /** * Listen for configuration changes */ listenForConfigChanges() { document.addEventListener('smartPollingConfigChanged', (e) => { const newConfig = e.detail; // Update intervals this.options.intervals = newConfig.intervals; this.options.debug = newConfig.debug; this.log('Configuration updated, adjusting polling frequency'); this.adjustPollingFrequency(); }); } /** * Initialize the Page Visibility API for background tab detection */ initializeVisibilityAPI() { // Handle different browser implementations of Page Visibility API if (typeof document.hidden !== "undefined") { this.visibilityChange = "visibilitychange"; this.hidden = "hidden"; } else if (typeof document.msHidden !== "undefined") { this.visibilityChange = "msvisibilitychange"; this.hidden = "msHidden"; } else if (typeof document.webkitHidden !== "undefined") { this.visibilityChange = "webkitvisibilitychange"; this.hidden = "webkitHidden"; } if (this.visibilityChange) { document.addEventListener(this.visibilityChange, this.handleVisibilityChange, false); } // Also listen for window focus/blur as backup window.addEventListener('focus', () => { this.isTabVisible = true; this.adjustPollingFrequency(); }); window.addEventListener('blur', () => { this.isTabVisible = false; this.adjustPollingFrequency(); }); } /** * Handle visibility change events */ handleVisibilityChange() { this.isTabVisible = !document[this.hidden]; this.log(`Tab visibility changed: ${this.isTabVisible ? 'visible' : 'hidden'}`); this.adjustPollingFrequency(); } /** * Start the smart polling service */ start() { if (this.isActive) { this.log('Polling already active'); return; } this.isActive = true; this.consecutiveErrors = 0; this.log('Starting smart polling service'); // Add a small delay for the initial poll to ensure DOM and fixtures are ready setTimeout(() => { if (this.isActive) { this.poll(); } }, 500); // 500ms delay for initial poll } /** * Stop the smart polling service */ stop() { if (!this.isActive) { return; } this.isActive = false; if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } this.log('Smart polling service stopped'); this.notifyStateChange('stopped'); } /** * Update the date being polled */ updateDate(newDate) { this.options.date = newDate; this.log(`Updated polling date to: ${newDate}`); if (this.isActive) { // Clear current timeout and start fresh poll cycle if (this.timeoutId) { clearTimeout(this.timeoutId); } this.poll(); } } /** * Main polling function */ async poll() { if (!this.isActive) { return; } try { this.log(`Polling for updates (mode: ${this.currentPollingMode})`); // Build URL with league filtering if lazy loading is active let pollUrl = `/public/ajax/match-status.php?date=${this.options.date}`; if (window.lazyFixtures && window.lazyFixtures.initialized) { const loadedLeagues = window.lazyFixtures.getLoadedLeagues(); if (loadedLeagues.length > 0) { pollUrl += `&league_ids=${loadedLeagues.join(',')}`; } } // Fetch match status updates const response = await fetch(pollUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-AJAX-Token': window.ajaxToken || '' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.success) { this.consecutiveErrors = 0; this.lastUpdateTime = new Date(); // Always use fixed 30 second interval const hasLiveMatches = data.summary?.has_live || false; // Always trigger onUpdate callback if (this.options.onUpdate) { this.options.onUpdate(data); } } else { throw new Error(data.error || 'Unknown error occurred'); } } catch (error) { this.consecutiveErrors++; this.log(`Polling error (${this.consecutiveErrors}/${this.maxErrors}): ${error.message}`); if (this.consecutiveErrors >= this.maxErrors) { this.log('Max errors reached, stopping polling'); this.stop(); return; } } // Schedule next poll this.scheduleNextPoll(); } /** * Analyze match states to determine optimal polling frequency */ analyzeMatchStates(matches) { if (!matches || matches.length === 0) { return 'noMatches'; } const now = new Date(); const twoHoursFromNow = new Date(now.getTime() + (2 * 60 * 60 * 1000)); const tenMinutesAgo = new Date(now.getTime() - (10 * 60 * 1000)); // 10 minutes ago let hasLiveMatches = false; let hasUpcomingMatches = false; let hasRecentlyFinishedMatches = false; for (const match of matches) { const status = match.status_short; // Check for live matches if (['1H', '2H', 'HT', 'ET', 'BT', 'P', 'LIVE'].includes(status)) { hasLiveMatches = true; break; // Live matches take priority } // Check for recently finished matches (finished in last 10 minutes) if (['FT', 'AET', 'PEN'].includes(status) && match.datetime) { const matchTime = new Date(match.datetime); // Estimate finish time as 2 hours after kick-off (rough estimate) const estimatedFinishTime = new Date(matchTime.getTime() + (2 * 60 * 60 * 1000)); if (estimatedFinishTime >= tenMinutesAgo) { hasRecentlyFinishedMatches = true; } } // Check for upcoming matches within 2 hours if (status === 'NS' && match.datetime) { const matchTime = new Date(match.datetime); if (matchTime <= twoHoursFromNow && matchTime > now) { hasUpcomingMatches = true; } } } if (hasLiveMatches) { return 'liveMatches'; } else if (hasRecentlyFinishedMatches) { // Keep polling recently finished matches at live frequency for a few minutes return 'liveMatches'; } else if (hasUpcomingMatches) { return 'upcomingMatches'; } else { return 'noMatches'; } } /** * Update the current polling mode and frequency */ updatePollingMode(newMode) { if (this.currentPollingMode !== newMode) { this.log(`Polling mode changed: ${this.currentPollingMode} -> ${newMode}`); this.currentPollingMode = newMode; this.notifyStateChange(newMode); } this.adjustPollingFrequency(); } /** * Adjust polling frequency - always use 30 seconds */ adjustPollingFrequency() { // Always use 30 second interval, regardless of match states or tab visibility const interval = 30000; // 30 seconds if (this.currentInterval !== interval) { this.currentInterval = interval; } } /** * Schedule the next poll */ scheduleNextPoll() { if (!this.isActive) { return; } this.adjustPollingFrequency(); this.timeoutId = setTimeout(() => { this.poll(); }, this.currentInterval); } /** * Notify about state changes */ notifyStateChange(state) { if (this.options.onStateChange) { this.options.onStateChange({ state, interval: this.currentInterval, mode: this.currentPollingMode, isTabVisible: this.isTabVisible, lastUpdate: this.lastUpdateTime }); } } /** * Get current polling status */ getStatus() { return { isActive: this.isActive, currentMode: this.currentPollingMode, currentInterval: this.currentInterval, isTabVisible: this.isTabVisible, lastUpdateTime: this.lastUpdateTime, consecutiveErrors: this.consecutiveErrors, date: this.options.date }; } /** * Log debug messages */ log(message) { if (this.options.debug) { console.log(`[SmartPolling] ${message}`); } } /** * Clean up event listeners */ destroy() { this.stop(); if (this.visibilityChange) { document.removeEventListener(this.visibilityChange, this.handleVisibilityChange); } window.removeEventListener('focus', this.handleVisibilityChange); window.removeEventListener('blur', this.handleVisibilityChange); this.log('Smart Polling Service destroyed'); } } // Make the class globally available window.SmartPollingService = SmartPollingService;