318 lines
9.7 KiB
JavaScript
318 lines
9.7 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|