freealberta/map/app/public/js/cache-manager.js
2025-08-22 14:45:40 -06:00

318 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Client-side cache management utility
* Handles cache busting and version checking for the application
*/
class ClientCacheManager {
constructor() {
this.currentVersion = null;
this.versionCheckInterval = null;
this.storageKey = 'app-version';
this.init();
}
/**
* Initialize cache manager
*/
init() {
this.getCurrentVersion();
this.startVersionChecking();
this.setupBeforeUnload();
}
/**
* Get current app version from meta tag or API
*/
async getCurrentVersion() {
try {
// First try to get version from meta tag
const metaVersion = document.querySelector('meta[name="app-version"]');
if (metaVersion) {
this.currentVersion = metaVersion.getAttribute('content');
this.storeVersion(this.currentVersion);
return this.currentVersion;
}
// Fallback to API call
const response = await fetch('/api/version');
if (response.ok) {
const data = await response.json();
this.currentVersion = data.version;
this.storeVersion(this.currentVersion);
return this.currentVersion;
}
} catch (error) {
console.warn('Could not retrieve app version:', error);
}
return null;
}
/**
* Store version in localStorage
* @param {string} version - Version to store
*/
storeVersion(version) {
try {
localStorage.setItem(this.storageKey, version);
} catch (error) {
// Ignore localStorage errors
}
}
/**
* Get stored version from localStorage
* @returns {string|null} Stored version
*/
getStoredVersion() {
try {
return localStorage.getItem(this.storageKey);
} catch (error) {
return null;
}
}
/**
* Check if app version has changed
* @returns {boolean} True if version changed
*/
async hasVersionChanged() {
const storedVersion = this.getStoredVersion();
const currentVersion = await this.getCurrentVersion();
return storedVersion && currentVersion && storedVersion !== currentVersion;
}
/**
* Force reload the page with cache busting
*/
forceReload() {
// Clear cache-related storage
try {
localStorage.removeItem(this.storageKey);
sessionStorage.clear();
} catch (error) {
// Ignore errors
}
// Force reload with cache busting
const url = new URL(window.location);
url.searchParams.set('_cb', Date.now());
window.location.replace(url.toString());
}
/**
* Start periodic version checking
*/
startVersionChecking() {
// Check every 60 seconds (reduced from 30 to ease rate limiting)
this.versionCheckInterval = setInterval(async () => {
try {
if (await this.hasVersionChanged()) {
this.handleVersionChange();
}
} catch (error) {
console.warn('Version check failed:', error);
// If we get a rate limit error, slow down checks
if (error.message?.includes('429') || error.message?.includes('Too Many Requests')) {
console.log('Slowing down version checks due to rate limiting');
clearInterval(this.versionCheckInterval);
// Restart with a longer interval (2 minutes)
this.versionCheckInterval = setInterval(async () => {
try {
if (await this.hasVersionChanged()) {
this.handleVersionChange();
}
} catch (error) {
console.warn('Version check failed:', error);
}
}, 120000); // 2 minutes
}
}
}, 60000); // 1 minute
}
/**
* Stop version checking
*/
stopVersionChecking() {
if (this.versionCheckInterval) {
clearInterval(this.versionCheckInterval);
this.versionCheckInterval = null;
}
}
/**
* Handle version change detection
*/
handleVersionChange() {
// Show update notification
this.showUpdateNotification();
}
/**
* Show update notification to user
*/
showUpdateNotification() {
// Remove existing notification
const existingNotification = document.querySelector('.update-notification');
if (existingNotification) {
existingNotification.remove();
}
// Create notification element
const notification = document.createElement('div');
notification.className = 'update-notification';
notification.innerHTML = `
<div class="update-notification-content">
<span class="update-message">
🔄 A new version is available!<br>
Please clear your browser cache and cookies, then refresh this page.<br>
<small>
<b>Instructions:</b> On most browsers, press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd> (Windows) or <kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd> (Mac) to force refresh.<br>
For best results, consider deleting browsing data via your browser settings.
</small>
</span>
<button class="update-dismiss" onclick="this.closest('.update-notification').remove()">×</button>
</div>
`;
// Add styles
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
animation: slideIn 0.3s ease-out;
`;
// Add animation styles
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.update-notification-content {
display: flex;
align-items: center;
gap: 10px;
}
.update-button {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.3);
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.update-button:hover {
background: rgba(255,255,255,0.3);
}
.update-dismiss {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 18px;
padding: 0;
margin-left: 5px;
}
.update-message {
font-size: 14px;
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// Auto-dismiss after 10 seconds
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 10000);
}
/**
* Setup beforeunload handler to check version on page refresh
*/
setupBeforeUnload() {
window.addEventListener('beforeunload', async () => {
// Quick version check before unload
try {
if (await this.hasVersionChanged()) {
// Clear cached version to force fresh load
this.storeVersion(null);
}
} catch (error) {
// Ignore errors during unload
}
});
}
/**
* Manual cache clear function
*/
clearCache() {
try {
// Clear localStorage
localStorage.clear();
// Clear sessionStorage
sessionStorage.clear();
// Clear service worker cache if available
if ('serviceWorker' in navigator && 'caches' in window) {
caches.keys().then(names => {
names.forEach(name => {
caches.delete(name);
});
});
}
console.log('Cache cleared successfully');
return true;
} catch (error) {
console.error('Failed to clear cache:', error);
return false;
}
}
/**
* Get debug information
* @returns {object} Debug info
*/
getDebugInfo() {
return {
currentVersion: this.currentVersion,
storedVersion: this.getStoredVersion(),
versionCheckActive: !!this.versionCheckInterval,
timestamp: new Date().toISOString()
};
}
}
// Initialize cache manager when DOM is ready
let cacheManager;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
cacheManager = new ClientCacheManager();
});
} else {
cacheManager = new ClientCacheManager();
}
// Make cache manager globally available for debugging
window.cacheManager = cacheManager;
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = ClientCacheManager;
}