feat: implement comprehensive PWA force update system
- Add UpdateManager and UpdateChecker for automatic version detection - Add post-build script for meta.json generation and version injection - Enhance Service Worker with version-aware caching - Add .htaccess configuration for proper cache control This ensures all users receive the latest version after deployment without manual cache clearing.
This commit is contained in:
39
src/app.jsx
39
src/app.jsx
@@ -1924,7 +1924,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.53 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
|
||||
handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.55 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (event.type === 'beforeunload' && !isTabSwitching) {
|
||||
@@ -3747,9 +3747,25 @@
|
||||
|
||||
]);
|
||||
};
|
||||
// UpdateChecker компонент для автоматической проверки обновлений
|
||||
const UpdateCheckerWrapper = ({ children }) => {
|
||||
// Проверяем доступность UpdateChecker
|
||||
if (typeof window !== 'undefined' && window.UpdateChecker) {
|
||||
return React.createElement(window.UpdateChecker, {
|
||||
debug: false
|
||||
}, children);
|
||||
}
|
||||
// Fallback если UpdateChecker не загружен
|
||||
return children;
|
||||
};
|
||||
|
||||
function initializeApp() {
|
||||
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||
// Оборачиваем приложение в UpdateChecker для автоматической проверки обновлений
|
||||
const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
|
||||
React.createElement(EnhancedSecureP2PChat)
|
||||
);
|
||||
ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
|
||||
} else {
|
||||
console.error('Модули не загружены:', {
|
||||
hasCrypto: !!window.EnhancedSecureCryptoUtils,
|
||||
@@ -3776,5 +3792,20 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Render Enhanced Application
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||
// Render Enhanced Application with UpdateChecker
|
||||
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
||||
const UpdateCheckerWrapper = ({ children }) => {
|
||||
if (typeof window !== 'undefined' && window.UpdateChecker) {
|
||||
return React.createElement(window.UpdateChecker, {
|
||||
debug: false
|
||||
}, children);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
|
||||
React.createElement(EnhancedSecureP2PChat)
|
||||
);
|
||||
ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
|
||||
} else {
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
||||
}
|
||||
290
src/components/UpdateChecker.jsx
Normal file
290
src/components/UpdateChecker.jsx
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* UpdateChecker - React component for automatic update checking
|
||||
*
|
||||
* Wraps the application and automatically detects new versions,
|
||||
* showing a modal window with update progress
|
||||
*/
|
||||
|
||||
const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
const [updateState, setUpdateState] = React.useState({
|
||||
hasUpdate: false,
|
||||
isUpdating: false,
|
||||
progress: 0,
|
||||
currentVersion: null,
|
||||
newVersion: null,
|
||||
showModal: false
|
||||
});
|
||||
|
||||
const updateManagerRef = React.useRef(null);
|
||||
|
||||
// Initialize UpdateManager
|
||||
React.useEffect(() => {
|
||||
// Check that UpdateManager is available
|
||||
if (typeof window === 'undefined' || !window.UpdateManager) {
|
||||
console.error('❌ UpdateManager not found. Make sure updateManager.js is loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create UpdateManager instance
|
||||
updateManagerRef.current = new window.UpdateManager({
|
||||
versionUrl: '/meta.json',
|
||||
checkInterval: 60000, // 1 minute
|
||||
checkOnLoad: true,
|
||||
debug: debug,
|
||||
onUpdateAvailable: (updateInfo) => {
|
||||
setUpdateState(prev => ({
|
||||
...prev,
|
||||
hasUpdate: true,
|
||||
currentVersion: updateInfo.currentVersion,
|
||||
newVersion: updateInfo.newVersion,
|
||||
showModal: true
|
||||
}));
|
||||
|
||||
// Call external callback if available
|
||||
if (onUpdateAvailable) {
|
||||
onUpdateAvailable(updateInfo);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (debug) {
|
||||
console.warn('Update check error (non-critical):', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (updateManagerRef.current) {
|
||||
updateManagerRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [onUpdateAvailable, debug]);
|
||||
|
||||
// Force update handler
|
||||
const handleForceUpdate = async () => {
|
||||
if (!updateManagerRef.current || updateState.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdateState(prev => ({
|
||||
...prev,
|
||||
isUpdating: true,
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
try {
|
||||
// Simulate update progress
|
||||
const progressSteps = [
|
||||
{ progress: 10, message: 'Saving data...' },
|
||||
{ progress: 30, message: 'Clearing Service Worker caches...' },
|
||||
{ progress: 50, message: 'Unregistering Service Workers...' },
|
||||
{ progress: 70, message: 'Clearing browser cache...' },
|
||||
{ progress: 90, message: 'Updating version...' },
|
||||
{ progress: 100, message: 'Reloading application...' }
|
||||
];
|
||||
|
||||
for (const step of progressSteps) {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
setUpdateState(prev => ({
|
||||
...prev,
|
||||
progress: step.progress
|
||||
}));
|
||||
}
|
||||
|
||||
// Start force update
|
||||
await updateManagerRef.current.forceUpdate();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Update failed:', error);
|
||||
setUpdateState(prev => ({
|
||||
...prev,
|
||||
isUpdating: false,
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
// Show error to user
|
||||
alert('Update error. Please refresh the page manually (Ctrl+F5 or Cmd+Shift+R)');
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal (not recommended, but leaving the option)
|
||||
const handleCloseModal = () => {
|
||||
// Warn user
|
||||
if (window.confirm('New version available. Update is recommended for security and stability. Continue without update?')) {
|
||||
setUpdateState(prev => ({
|
||||
...prev,
|
||||
showModal: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Format version for display
|
||||
const formatVersion = (version) => {
|
||||
if (!version) return 'N/A';
|
||||
// If version is timestamp, format as date
|
||||
if (/^\d+$/.test(version)) {
|
||||
const date = new Date(parseInt(version));
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
return React.createElement(React.Fragment, null, [
|
||||
// Main application content
|
||||
children,
|
||||
|
||||
// Update modal window
|
||||
updateState.showModal && React.createElement('div', {
|
||||
key: 'update-modal',
|
||||
className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm',
|
||||
style: {
|
||||
animation: 'fadeIn 0.3s ease-in-out'
|
||||
}
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal-content',
|
||||
className: 'bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-gray-200 dark:border-gray-700',
|
||||
style: {
|
||||
animation: 'slideUp 0.3s ease-out'
|
||||
}
|
||||
}, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'text-center mb-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-16 h-16 mx-auto mb-4 bg-blue-500/10 rounded-full flex items-center justify-center'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon-fa',
|
||||
className: 'fas fa-sync-alt text-blue-500 text-2xl animate-spin'
|
||||
})
|
||||
]),
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl font-bold text-gray-900 dark:text-white mb-2'
|
||||
}, 'Update Available'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-gray-600 dark:text-gray-300 text-sm'
|
||||
}, 'A new version of the application has been detected')
|
||||
]),
|
||||
|
||||
// Version information
|
||||
React.createElement('div', {
|
||||
key: 'version-info',
|
||||
className: 'bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-6 space-y-2'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'current',
|
||||
className: 'flex justify-between items-center'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'current-label',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'Current version:'),
|
||||
React.createElement('span', {
|
||||
key: 'current-value',
|
||||
className: 'text-sm font-mono text-gray-900 dark:text-white'
|
||||
}, formatVersion(updateState.currentVersion))
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'new',
|
||||
className: 'flex justify-between items-center'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'new-label',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'New version:'),
|
||||
React.createElement('span', {
|
||||
key: 'new-value',
|
||||
className: 'text-sm font-mono text-blue-600 dark:text-blue-400 font-semibold'
|
||||
}, formatVersion(updateState.newVersion))
|
||||
])
|
||||
]),
|
||||
|
||||
// Update progress
|
||||
updateState.isUpdating && React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'mb-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: 'w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-2'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-fill',
|
||||
className: 'bg-blue-500 h-2.5 rounded-full transition-all duration-300',
|
||||
style: {
|
||||
width: `${updateState.progress}%`
|
||||
}
|
||||
})
|
||||
]),
|
||||
React.createElement('p', {
|
||||
key: 'progress-text',
|
||||
className: 'text-center text-sm text-gray-600 dark:text-gray-400'
|
||||
}, `${updateState.progress}%`)
|
||||
]),
|
||||
|
||||
// Action buttons
|
||||
!updateState.isUpdating && React.createElement('div', {
|
||||
key: 'actions',
|
||||
className: 'flex gap-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'update-btn',
|
||||
onClick: handleForceUpdate,
|
||||
className: 'flex-1 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2',
|
||||
disabled: updateState.isUpdating
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'update-icon',
|
||||
className: 'fas fa-download'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'update-text'
|
||||
}, 'Update Now')
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'close-btn',
|
||||
onClick: handleCloseModal,
|
||||
className: 'px-4 py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-200',
|
||||
disabled: updateState.isUpdating
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'close-icon',
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Update indicator
|
||||
updateState.isUpdating && React.createElement('div', {
|
||||
key: 'updating',
|
||||
className: 'text-center'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
key: 'updating-text',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'Update in progress...')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = UpdateChecker;
|
||||
} else {
|
||||
window.UpdateChecker = UpdateChecker;
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.7.53')
|
||||
}, 'End-to-end freedom v4.7.55')
|
||||
])
|
||||
]),
|
||||
|
||||
|
||||
540
src/utils/updateManager.js
Normal file
540
src/utils/updateManager.js
Normal file
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* UpdateManager - Comprehensive PWA update management system
|
||||
*
|
||||
* Automatically detects new application versions and forcefully
|
||||
* updates all cache levels: Service Worker, browser cache, localStorage
|
||||
*
|
||||
* @class UpdateManager
|
||||
*/
|
||||
class UpdateManager {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
// URL for version check (meta.json)
|
||||
versionUrl: options.versionUrl || '/meta.json',
|
||||
|
||||
// Update check interval (ms)
|
||||
checkInterval: options.checkInterval || 60000, // 1 minute
|
||||
|
||||
// Local storage key for version
|
||||
versionKey: options.versionKey || 'app_version',
|
||||
|
||||
// Keys for preserving critical data before cleanup
|
||||
preserveKeys: options.preserveKeys || [
|
||||
'auth_token',
|
||||
'user_settings',
|
||||
'encryption_keys',
|
||||
'peer_connections'
|
||||
],
|
||||
|
||||
// Callback on update detection
|
||||
onUpdateAvailable: options.onUpdateAvailable || null,
|
||||
|
||||
// Callback on error
|
||||
onError: options.onError || null,
|
||||
|
||||
// Logging
|
||||
debug: options.debug || false,
|
||||
|
||||
// Force check on load
|
||||
checkOnLoad: options.checkOnLoad !== false,
|
||||
|
||||
// Request timeout
|
||||
requestTimeout: options.requestTimeout || 10000
|
||||
};
|
||||
|
||||
this.currentVersion = null;
|
||||
this.serverVersion = null;
|
||||
this.checkIntervalId = null;
|
||||
this.isUpdating = false;
|
||||
this.updatePromise = null;
|
||||
|
||||
// Initialization
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize update manager
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// Load current version from localStorage
|
||||
this.currentVersion = this.getLocalVersion();
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('🔄 UpdateManager initialized', {
|
||||
currentVersion: this.currentVersion,
|
||||
versionUrl: this.options.versionUrl
|
||||
});
|
||||
}
|
||||
|
||||
// Check version on load
|
||||
if (this.options.checkOnLoad) {
|
||||
await this.checkForUpdates();
|
||||
}
|
||||
|
||||
// Start periodic check
|
||||
this.startPeriodicCheck();
|
||||
|
||||
// Listen to Service Worker events
|
||||
this.setupServiceWorkerListeners();
|
||||
|
||||
} catch (error) {
|
||||
this.handleError('Init failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local version from localStorage
|
||||
*/
|
||||
getLocalVersion() {
|
||||
try {
|
||||
return localStorage.getItem(this.options.versionKey) || null;
|
||||
} catch (error) {
|
||||
this.handleError('Failed to get local version', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save version to localStorage
|
||||
*/
|
||||
setLocalVersion(version) {
|
||||
try {
|
||||
localStorage.setItem(this.options.versionKey, version);
|
||||
this.currentVersion = version;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Version saved:', version);
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError('Failed to save version', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates on server
|
||||
*/
|
||||
async checkForUpdates() {
|
||||
// Prevent parallel checks
|
||||
if (this.updatePromise) {
|
||||
return this.updatePromise;
|
||||
}
|
||||
|
||||
this.updatePromise = this._performCheck();
|
||||
const result = await this.updatePromise;
|
||||
this.updatePromise = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform version check
|
||||
*/
|
||||
async _performCheck() {
|
||||
try {
|
||||
if (this.options.debug) {
|
||||
console.log('🔍 Checking for updates...');
|
||||
}
|
||||
|
||||
// Request meta.json with cache-busting
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.options.versionUrl}?t=${Date.now()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const meta = await response.json();
|
||||
this.serverVersion = meta.version || meta.buildVersion || null;
|
||||
|
||||
if (!this.serverVersion) {
|
||||
throw new Error('Version not found in meta.json');
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('📦 Server version:', this.serverVersion, 'Local:', this.currentVersion);
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
if (this.currentVersion === null) {
|
||||
// First load - save version
|
||||
this.setLocalVersion(this.serverVersion);
|
||||
return { hasUpdate: false, version: this.serverVersion };
|
||||
}
|
||||
|
||||
if (this.currentVersion !== this.serverVersion) {
|
||||
// New version detected
|
||||
if (this.options.debug) {
|
||||
console.log('🆕 New version detected!', {
|
||||
current: this.currentVersion,
|
||||
new: this.serverVersion
|
||||
});
|
||||
}
|
||||
|
||||
// Call callback
|
||||
if (this.options.onUpdateAvailable) {
|
||||
this.options.onUpdateAvailable({
|
||||
currentVersion: this.currentVersion,
|
||||
newVersion: this.serverVersion,
|
||||
updateManager: this
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hasUpdate: true,
|
||||
currentVersion: this.currentVersion,
|
||||
newVersion: this.serverVersion
|
||||
};
|
||||
}
|
||||
|
||||
return { hasUpdate: false, version: this.serverVersion };
|
||||
|
||||
} catch (error) {
|
||||
// Graceful degradation - if meta.json is unavailable, continue working
|
||||
if (this.options.debug) {
|
||||
console.warn('⚠️ Update check failed (non-critical):', error.message);
|
||||
}
|
||||
|
||||
if (this.options.onError) {
|
||||
this.options.onError(error);
|
||||
}
|
||||
|
||||
return { hasUpdate: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with timeout
|
||||
*/
|
||||
async fetchWithTimeout(url, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.options.requestTimeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force application update
|
||||
* Clears all cache levels and reloads the page
|
||||
*/
|
||||
async forceUpdate() {
|
||||
if (this.isUpdating) {
|
||||
if (this.options.debug) {
|
||||
console.log('⏳ Update already in progress...');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
if (this.options.debug) {
|
||||
console.log('🚀 Starting force update...');
|
||||
}
|
||||
|
||||
// Step 1: Preserve critical data
|
||||
const preservedData = this.preserveCriticalData();
|
||||
|
||||
// Step 2: Clear Service Worker caches
|
||||
await this.clearServiceWorkerCaches();
|
||||
|
||||
// Step 3: Unregister Service Workers
|
||||
await this.unregisterServiceWorkers();
|
||||
|
||||
// Step 4: Clear browser cache (localStorage, sessionStorage)
|
||||
this.clearBrowserCaches();
|
||||
|
||||
// Step 5: Update version
|
||||
if (this.serverVersion) {
|
||||
this.setLocalVersion(this.serverVersion);
|
||||
}
|
||||
|
||||
// Step 6: Restore critical data
|
||||
this.restoreCriticalData(preservedData);
|
||||
|
||||
// Step 7: Force reload with cache-busting
|
||||
if (this.options.debug) {
|
||||
console.log('🔄 Reloading page with new version...');
|
||||
}
|
||||
|
||||
// Small delay to complete operations
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Reload with full cache bypass
|
||||
window.location.href = `${window.location.pathname}?v=${Date.now()}&_update=true`;
|
||||
|
||||
} catch (error) {
|
||||
this.handleError('Force update failed', error);
|
||||
this.isUpdating = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve critical data before cleanup
|
||||
*/
|
||||
preserveCriticalData() {
|
||||
const data = {};
|
||||
|
||||
this.options.preserveKeys.forEach(key => {
|
||||
try {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value !== null) {
|
||||
data[key] = value;
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
console.warn(`⚠️ Failed to preserve ${key}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('💾 Preserved critical data:', Object.keys(data));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore critical data after cleanup
|
||||
*/
|
||||
restoreCriticalData(data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
console.warn(`⚠️ Failed to restore ${key}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Restored critical data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all Service Worker caches
|
||||
*/
|
||||
async clearServiceWorkerCaches() {
|
||||
try {
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('🗑️ Clearing Service Worker caches:', cacheNames);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
cacheNames.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
|
||||
// Send message to Service Worker for cleanup
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'CACHE_CLEAR'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Service Worker caches cleared');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError('Failed to clear SW caches', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all Service Workers
|
||||
*/
|
||||
async unregisterServiceWorkers() {
|
||||
try {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('🔌 Unregistering Service Workers:', registrations.length);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
registrations.map(registration => {
|
||||
// Send skipWaiting command before unregistering
|
||||
if (registration.waiting) {
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
if (registration.installing) {
|
||||
registration.installing.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
return registration.unregister();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Service Workers unregistered');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError('Failed to unregister SW', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear browser caches (localStorage, sessionStorage)
|
||||
*/
|
||||
clearBrowserCaches() {
|
||||
try {
|
||||
// Clear sessionStorage
|
||||
sessionStorage.clear();
|
||||
|
||||
// Clear localStorage (except critical data that is already preserved)
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && !this.options.preserveKeys.includes(key) && key !== this.options.versionKey) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
console.warn(`⚠️ Failed to remove ${key}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Browser caches cleared');
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError('Failed to clear browser caches', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic update check
|
||||
*/
|
||||
startPeriodicCheck() {
|
||||
if (this.checkIntervalId) {
|
||||
clearInterval(this.checkIntervalId);
|
||||
}
|
||||
|
||||
this.checkIntervalId = setInterval(() => {
|
||||
this.checkForUpdates();
|
||||
}, this.options.checkInterval);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`⏰ Periodic check started (${this.options.checkInterval}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic check
|
||||
*/
|
||||
stopPeriodicCheck() {
|
||||
if (this.checkIntervalId) {
|
||||
clearInterval(this.checkIntervalId);
|
||||
this.checkIntervalId = null;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('⏹️ Periodic check stopped');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Service Worker event listeners
|
||||
*/
|
||||
setupServiceWorkerListeners() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Listen to Service Worker updates
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (this.options.debug) {
|
||||
console.log('🔄 Service Worker controller changed');
|
||||
}
|
||||
|
||||
// Check for updates after controller change
|
||||
setTimeout(() => {
|
||||
this.checkForUpdates();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Listen to messages from Service Worker
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SW_ACTIVATED') {
|
||||
if (this.options.debug) {
|
||||
console.log('✅ Service Worker activated');
|
||||
}
|
||||
|
||||
// Check for updates after activation
|
||||
setTimeout(() => {
|
||||
this.checkForUpdates();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors
|
||||
*/
|
||||
handleError(message, error) {
|
||||
const errorMessage = `${message}: ${error.message || error}`;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.error('❌ UpdateManager error:', errorMessage, error);
|
||||
}
|
||||
|
||||
if (this.options.onError) {
|
||||
this.options.onError(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy manager (cleanup)
|
||||
*/
|
||||
destroy() {
|
||||
this.stopPeriodicCheck();
|
||||
this.updatePromise = null;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log('🗑️ UpdateManager destroyed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = UpdateManager;
|
||||
} else {
|
||||
window.UpdateManager = UpdateManager;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user