Really-amin's picture
Upload 577 files
b190b45 verified
/**
* Polling Manager
* Replaces WebSocket with intelligent HTTP polling
*
* Features:
* - Multiple concurrent polls with different intervals
* - Auto-pause when page is hidden (Page Visibility API)
* - Manual start/stop control
* - Last update timestamp tracking
* - Error handling and retry
*/
export class PollingManager {
constructor() {
this.polls = new Map();
this.lastUpdates = new Map();
this.isVisible = !document.hidden;
this.updateCallbacks = new Map();
// Listen to page visibility changes
document.addEventListener('visibilitychange', () => {
this.isVisible = !document.hidden;
console.log(`[PollingManager] Page visibility changed: ${this.isVisible ? 'visible' : 'hidden'}`);
if (this.isVisible) {
this.resumeAll();
} else {
this.pauseAll();
}
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
this.stopAll();
});
console.log('[PollingManager] Initialized');
}
/**
* Start polling an endpoint
* @param {string} key - Unique identifier for this poll
* @param {Function} fetchFunction - Async function that fetches data
* @param {Function} callback - Function to call with fetched data
* @param {number} interval - Polling interval in milliseconds
*/
start(key, fetchFunction, callback, interval) {
// Stop existing poll if any
this.stop(key);
const poll = {
fetchFunction,
callback,
interval,
timerId: null,
isPaused: false,
errorCount: 0,
consecutiveErrors: 0,
maxConsecutiveErrors: 5,
};
// Initial fetch (don't wait for interval)
this._executePoll(key, poll);
// Setup recurring interval
poll.timerId = setInterval(() => {
if (!poll.isPaused && this.isVisible) {
this._executePoll(key, poll);
}
}, interval);
this.polls.set(key, poll);
console.log(`[PollingManager] Started polling: ${key} every ${interval}ms`);
}
/**
* Execute a single poll
*/
async _executePoll(key, poll) {
try {
console.log(`[PollingManager] Fetching: ${key}`);
const data = await poll.fetchFunction();
// Reset error count on success
poll.consecutiveErrors = 0;
// Update timestamp
this.lastUpdates.set(key, Date.now());
// Call success callback
poll.callback(data, null);
// Notify update callbacks
this._notifyUpdateCallbacks(key);
} catch (error) {
poll.consecutiveErrors++;
poll.errorCount++;
console.error(`[PollingManager] Error in ${key} (${poll.consecutiveErrors}/${poll.maxConsecutiveErrors}):`, error);
// Call error callback
poll.callback(null, error);
// Stop polling after too many consecutive errors
if (poll.consecutiveErrors >= poll.maxConsecutiveErrors) {
console.error(`[PollingManager] Too many consecutive errors, stopping ${key}`);
this.stop(key);
}
}
}
/**
* Stop polling for a specific key
*/
stop(key) {
const poll = this.polls.get(key);
if (poll && poll.timerId) {
clearInterval(poll.timerId);
this.polls.delete(key);
this.lastUpdates.delete(key);
console.log(`[PollingManager] Stopped polling: ${key}`);
}
}
/**
* Pause a specific poll (keeps in memory, stops fetching)
*/
pause(key) {
const poll = this.polls.get(key);
if (poll) {
poll.isPaused = true;
console.log(`[PollingManager] Paused: ${key}`);
}
}
/**
* Resume a specific poll
*/
resume(key) {
const poll = this.polls.get(key);
if (poll) {
poll.isPaused = false;
// Immediate fetch on resume
this._executePoll(key, poll);
console.log(`[PollingManager] Resumed: ${key}`);
}
}
/**
* Pause all active polls (e.g., when page is hidden)
*/
pauseAll() {
console.log('[PollingManager] Pausing all polls');
for (const [key, poll] of this.polls) {
poll.isPaused = true;
}
}
/**
* Resume all paused polls (e.g., when page becomes visible)
*/
resumeAll() {
console.log('[PollingManager] Resuming all polls');
for (const [key, poll] of this.polls) {
if (poll.isPaused) {
poll.isPaused = false;
// Immediate fetch on resume
this._executePoll(key, poll);
}
}
}
/**
* Stop all polls and clear
*/
stopAll() {
console.log('[PollingManager] Stopping all polls');
for (const key of this.polls.keys()) {
this.stop(key);
}
}
/**
* Get last update timestamp for a poll
*/
getLastUpdate(key) {
return this.lastUpdates.get(key) || null;
}
/**
* Get formatted "last updated" string
*/
getLastUpdateText(key) {
const timestamp = this.getLastUpdate(key);
if (!timestamp) return 'Never';
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 5) return 'Just now';
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
/**
* Check if a poll is active
*/
isActive(key) {
return this.polls.has(key);
}
/**
* Check if a poll is paused
*/
isPaused(key) {
const poll = this.polls.get(key);
return poll ? poll.isPaused : false;
}
/**
* Get all active poll keys
*/
getActivePolls() {
return Array.from(this.polls.keys());
}
/**
* Get poll info
*/
getPollInfo(key) {
const poll = this.polls.get(key);
if (!poll) return null;
return {
key,
interval: poll.interval,
isPaused: poll.isPaused,
errorCount: poll.errorCount,
consecutiveErrors: poll.consecutiveErrors,
lastUpdate: this.getLastUpdateText(key),
isActive: true,
};
}
/**
* Register callback for last update changes
* Returns unsubscribe function
*/
onLastUpdate(callback) {
const id = Date.now() + Math.random();
this.updateCallbacks.set(id, callback);
// Return unsubscribe function
return () => this.updateCallbacks.delete(id);
}
/**
* Notify all update callbacks
*/
_notifyUpdateCallbacks(key) {
const text = this.getLastUpdateText(key);
for (const callback of this.updateCallbacks.values()) {
try {
callback(key, text);
} catch (error) {
console.error('[PollingManager] Error in update callback:', error);
}
}
}
/**
* Update all UI elements showing "last updated"
* Call this in an interval (e.g., every second)
*/
updateAllLastUpdateTexts() {
for (const key of this.polls.keys()) {
this._notifyUpdateCallbacks(key);
}
}
}
// ============================================================================
// EXPORT SINGLETON INSTANCE
// ============================================================================
export const pollingManager = new PollingManager();
// Auto-update "last updated" text every second
setInterval(() => {
pollingManager.updateAllLastUpdateTexts();
}, 1000);
export default pollingManager;